Desktop nightly 20260905.1284 closes its own local WebSocket ~100 ms after connect and retries every 16 s forever ("MacBook Pro (2) is reconnecting")
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 23k
- Forks
- 5.9k
- Avg merge
- 11h 14m
- Merged PRs (30d)
- 357
Description
What happened
On the T3 Code Nightly desktop app, trying to start a new thread showed a banner in the composer: "MacBook Pro (2) is reconnecting — Trying again", with a "Reconnecting..." button and a "Connections" button. The user's reaction: "that macbook is this device, how is it failing to connect to my own device that it is running on". The mobile app connected to the same machine worked fine at the same time.
Diagnosis
The desktop renderer's WebSocket to its own embedded local server (127.0.0.1:3773, connectionMethod=direct) opens, authenticates, reaches the connected phase, and is torn down ~60-200 ms later. The client then retries on a fixed ~16 s cadence indefinitely and never recovers. It started at the exact instant the app auto-updated from 0.0.39-nightly.20260904.1280 to 0.0.39-nightly.20260905.1284, and downgrading to 1280 fixed it immediately.
Per-cycle behaviour (server trace, one representative cycle at 00:40:11 local). The /ws upgrade is accepted, SessionStore.verifyWebSocketToken / markConnected succeed, the client subscribes, and then every subscription is interrupted together by a client abort within ~100 ms of the upgrade:
ws.rpc.subscribeServerConfigInterrupted (55.8 ms; the underlyingrefreshAllhad just completed)ws.rpc.subscribeServerLifecycle,ws.rpc.previewAutomation.connect,ws.rpc.subscribeTerminalMetadata,ws.rpc.subscribeAuthAccessInterrupted (~30 ms each)ws.rpc.server.discoverSourceControlandws.rpc.subscribeVcsStatus(x3) Interrupted
Every one carries exit: {_tag: "Interrupted", cause: "InterruptError ... at ClientAbort"}. Nothing on the server fails first; the server sees the client go away. The whole /ws span is 57-196 ms (median 95 ms) across 147 consecutive attempts.
Why it never self-heals. packages/client-runtime/src/connection/supervisor.ts (main @ 7a089b2) line 33: RETRY_DELAYS_MS = [3_000, 4_000, 8_000, 16_000]; line 37: BACKOFF_RESET_AFTER_MS = 30_000; line 602 only marks an attempt stable when connectedForMs >= BACKOFF_RESET_AFTER_MS. A connection that lives ~100 ms is never stable, so the failure count pins at the ladder ceiling and the client retries every 16 s (observed median gap 16.1 s) with no path back to healthy. The observed cadence matches the code exactly.
Not a server-side problem. The mobile client (clientSurface=mobile, app 0.1.0) connected to the same server process during the same window and held a socket for 60.2 s while the desktop was dying at ~100 ms. The server continued serving RPCs normally.
Clean version boundary (strongest evidence). auth_sessions in ~/.t3/userdata/state.sqlite:
- session
fa26d53f…, client0.0.39-nightly.20260904.1280, issued2026-09-05T01:14:31Z, revoked2026-09-05T05:34:53Z— a 4 h 20 m session. (The triage pass saw the matching 15,611.7 s/wsspan in the server trace before that file rotated out, so it is corroborated here from the DB.) - session
9c3096ee…, client0.0.39-nightly.20260905.1284, issued2026-09-05T05:34:53Z— the same instant. Desktop trace showeddesktop.updates.applyDownloadProgressfiring at 05:34:40Z right before it. - Two further 1284 sessions (
7778fa6d…05:43:37Z,306dcf9e…06:09:29Z) = two more app restarts; the flap continued unbroken across all three. - All 147 sub-second desktop
/wsspans in the traces retained at the time were on 1284; there were zero desktop/wsspans of 1 s or longer on 1284.
Confirmed fix by downgrade. Reinstalling 0.0.39-nightly.20260904.1280 from the official DMG: session 8fda8f1b… (client 1280) issued 2026-09-05T06:19:38Z, still not revoked at 2026-09-05T16:28Z — a single unbroken 10 h 08 m session, with the renderer's socket to 127.0.0.1:3773 still ESTABLISHED. Immediately after the downgrade, ws.rpc.subscribeVcsStatus completed Success (34 of them, up to 98 s long) instead of Interrupted. This brackets the regression to nightly builds 1281-1284 (release commits d6e29dc9d 2026-09-04T18:38Z → 9cb40178a 2026-09-05T03:40Z, 87 commits).
Ruled out: two servers sharing one state dir. During triage a launchd background service (t3 serve 0.0.36 on :62973, com.t3tools.t3code.service) was found running alongside the app's embedded server on :3773, both advertising the same environmentId/label "MacBook Pro (2)" from the shared ~/.t3/userdata. Booting it out (launchctl bootout) and watching for 95 s changed nothing about the failure. Red herring.
Suspect commits (leads only, none confirmed). Commits in the 1280→1284 range touching the client connection lifecycle:
363cde411fix(connect): refresh HTTP credentials without reconnecting (#9594), merged 03:12Z. Touchessupervisor.ts,connection/layer.ts,connection/resolver.ts,authorization/service.ts, andapps/web/src/connection/{runtime,platform,storage}.ts. Reading the full diff: insupervisor.tsit deletes the "replacement connection" machinery (forkScopedTracedConnection,prepareReplacement, the connected-lease loop) and replaces it with a singleraceFirst(session.closed, monitorConnectedLease)running directly in the attempt'sEffect.scopedscope rather than a forked child scope. Inlayer.tsit movesRemoteEnvironmentAuthorization.layerout from under the resolver to an outerLayer.provideMerge, and that service now requiresScope.Scope. Inapps/web/src/connection/runtime.tsit changesLayer.merge(Connection.layerWithOptions(...), snapshotLoaderLayer)tosnapshotLoaderLayer.pipe(Layer.provideMerge(Connection.layerWithOptions(...))). Honest assessment: nothing in the diff explicitly closes a socket, and all of the new logic (token lock,assertSession, DPoP renewal) is relay-only; this is a direct bearer connection. The only mechanism visible is the scope/layer restructuring changing when the connection scope gets closed, and no specific line can be pointed at. Weak lead.2dca7a1edfix(client): explain possible network blocking for T3 Connect (#9783), 00:54Z —supervisor.tsandrpc/session.ts, but the diff is a relay-only error-message change. Very unlikely.98a29cbaafix: address usage limits and merge settlement regressions (#9784), 21:46Z — addsusageLimitSources: trueto the desktop'ssubscribeServerConfiginput inrpc/session.tsandapps/web/src/connection/runtime.ts. Worth noting because of the timing observation below.d7cf8aaa8perf(client): stop thread streams when unused (#9740) — lifecycle-adjacent, not examined in depth.
Timing observation (unconfirmed). In every cycle the socket died ~30-40 ms after ws.rpc.subscribeServerConfig's internal refreshAll completed, i.e. right when the first config snapshot would be delivered to the client. In rpc/session.ts a failure in the client-side config stream fails configSubscriptionClosed, which is one arm of session.closed, which the supervisor races on — and that teardown would produce exactly the all-subscriptions-Interrupted-by-ClientAbort pattern the server recorded. So "desktop client fails to process the first serverConfig snapshot from a 1284 server and tears the session down" is consistent with the evidence, but there is no client-side log to confirm it.
Known gap. The WebSocket close code/reason was not captured. The renderer console is not written to disk; the desktop main-process trace showed only IPC/update spans in the window; and apps/server/src/ws.ts on main has no server-initiated socket-close path (the only close-adjacent code is sessions.markDisconnected at line 2911 in the socket's finalizer, and the clientRemoved credential-change handler at line 374 emits an event rather than closing sockets). So the close originates client-side or in the transport. A loopback packet capture was not run because it would have required reinstalling the broken build on a machine that had just been fixed.
Note on evidence retention. server.trace.ndjson rotates fast on an active machine (10 files, ~10-15 MB each, a few hours total). All trace excerpts below were read from disk during the incident window; those files have since rotated out and are no longer recoverable on this machine. The auth_sessions rows are durable and were re-verified 10 hours later.
Steps to reproduce
Reproduced deterministically on this machine across three app launches; not attempted elsewhere.
- macOS (Apple Silicon), T3 Code Nightly with a local direct environment (embedded server on 127.0.0.1:3773).
- Auto-update, or install,
0.0.39-nightly.20260905.1284. - Open the app. Composer shows "<machine> is reconnecting — Trying again"; it never clears.
- Watch
~/.t3/userdata/logs/server.trace.ndjson:/wsspans forclientSurface=desktoplast ~100 ms and repeat every ~16 s; everyws.rpc.subscribe*span exitsInterruptedviaClientAbort. - Connect the mobile app to the same environment: it works.
- Reinstall
0.0.39-nightly.20260904.1280: socket stays open, subscriptions complete.
Version
Desktop: was 0.0.39-nightly.20260905.1284 (broken); now 0.0.39-nightly.20260904.1280 (working). Background service: t3 0.0.36 → 0.0.38 during the session. Source checked against main @ 7a089b2b2449c5c146e1a7d554a31e6d55924d3f.
Environment
macOS 26.6.2 (Darwin 25.6.0) arm64, Node v26.5.0. Surface: desktop app, connectionMethod=direct to its own local server at 127.0.0.1:3773 with a bearer session; mobile client 0.1.0 connected via relay to the same server.
Evidence
# ~/.t3/userdata/logs/server.trace.ndjson.* — desktop /ws spans (wsTicket stripped)
# 147 consecutive attempts, all clientAppVersion=0.0.39-nightly.20260905.1284, all < 1 s
00:39:55 89 ms gap=- url.query=clientSurface=desktop&clientAppVersion=0.0.39-nightly.20260905.1284&clientDeviceType=desktop&clientOs=macOS&connectionMethod=direct
00:40:11 107 ms gap=16.1 s
00:40:27 76 ms gap=16.1 s
00:40:43 87 ms gap=16.1 s
...
01:18:32 103 ms gap=16.1 s
01:18:48 95 ms gap=16.1 s
# duration min/median/max = 57 / 95 / 196 ms; median gap = 16.1 s
# same server process, mobile client, same window
01:06:03 60.2 s clientSurface=mobile clientAppVersion=0.1.0
# one desktop cycle, 00:40:11 (all exit._tag from the trace)
00:40:11.843 107.0ms http.server GET /ws (upgrade accepted; verifyWebSocketToken, markConnected OK)
00:40:11.850 55.8ms ws.rpc.subscribeServerConfig Interrupted
00:40:11.905 30.3ms ws.rpc.subscribeServerLifecycle Interrupted
00:40:11.907 34.0ms ws.rpc.previewAutomation.connect Interrupted
00:40:11.911 31.6ms ws.rpc.subscribeTerminalMetadata Interrupted
00:40:11.911 31.8ms ws.rpc.subscribeAuthAccess Interrupted
00:40:11.930 19.3ms ws.rpc.server.discoverSourceControl Interrupted
00:40:11.9xx ws.rpc.subscribeVcsStatus x3 Interrupted
# exit.cause: "InterruptError: All fibers interrupted without error at ClientAbort"
# ~/.t3/userdata/state.sqlite auth_sessions (ids truncated) — durable, re-verified 10 h later
fa26d53f 0.0.39-nightly.20260904.1280 issued 2026-09-05T01:14:31Z revoked 2026-09-05T05:34:53Z <- 4h20m healthy
9c3096ee 0.0.39-nightly.20260905.1284 issued 2026-09-05T05:34:53Z revoked 2026-09-05T05:43:37Z <- auto-update; flapping from here
7778fa6d 0.0.39-nightly.20260905.1284 issued 2026-09-05T05:43:37Z revoked 2026-09-05T06:09:29Z <- restart, still flapping
306dcf9e 0.0.39-nightly.20260905.1284 issued 2026-09-05T06:09:29Z revoked 2026-09-05T06:19:38Z <- restart, still flapping
8fda8f1b 0.0.39-nightly.20260904.1280 issued 2026-09-05T06:19:38Z revoked (null) <- downgrade; last_connected 16:28Z, 10h08m unbroken
# after downgrade, desktop subscriptions complete instead of aborting
01:19:40 ws.rpc.subscribeVcsStatus 32.3 s Success
01:20:13 ws.rpc.subscribeVcsStatus 55.4 s Success
01:22:04 ws.rpc.subscribeVcsStatus 98.2 s Success
# lsof at 14:53 (10 h after downgrade): T3 Code renderer -> 127.0.0.1:3773 ESTABLISHED
# packages/client-runtime/src/connection/supervisor.ts @ main 7a089b2
33: const RETRY_DELAYS_MS = [3_000, 4_000, 8_000, 16_000] as const;
37: const BACKOFF_RESET_AFTER_MS = 30_000;
602: stable: connectedForMs >= BACKOFF_RESET_AFTER_MS,
Related issues
#7231 (open) is the 15 s CONNECTION_PROBE_TIMEOUT path in monitorConnectedLease — it needs an application-active wakeup, surfaces "did not respond to a connection health check", and takes seconds; ours dies in ~100 ms with no probe involved. #3553 (closed) same probe family. #4671 / #4901 (closed) are Android RPC protocol skew against a newer server; here the client and embedded server are the same build and mobile works. #4773 (closed) is a CPU-bound backend; this server was idle and serving mobile fine. #3734 (closed) is a remote LAN environment reconnecting every 30-45 s, not a local direct socket at 100 ms. Newer: #9869 (Tailscale remote endpoint fetch fails after a nightly update) and #9685 (cannot pause a stuck remote reconnect) are remote-environment issues with different failure modes. No duplicate found.
Fix applied or workaround
- Downgraded the desktop app to
0.0.39-nightly.20260904.1280from the official DMG. Working; single unbroken session for 10 h 08 m at time of filing. - Updated the launchd background service from t3 0.0.36 to 0.0.38 (
~/.t3/runtime/versions/0.0.38,com.t3tools.t3code.service). Note thet3on PATH is an npx cache entry that still prints 0.0.36; the service itself runs 0.0.38. - The user's
~/.npmrchasmin-release-age=7, which was legitimately holding the service at 0.0.36. A one-offnpm_config_min_release_age=0was used for that single install; the setting was left in place. Not a T3 bug. - Booting out the launchd service was tested as a fix and did not help; it was restored.
Filed by
claude (fable-5.1 subagent, orchestrated by opus-5) via t3 triage
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Reproduce the failure on nightly 1284 and compare it with the working 1280, using packages/client-runtime/src/connection/supervisor.ts, connection/layer.ts, rpc/session.ts, and apps/web/src/connection/runtime.ts as entry points. Inspect the connection-scope and config-subscription changes around the listed commits, then use the server trace to verify the cause. Done means the desktop WebSocket remains connected and its subscriptions complete instead of being interrupted every 16 seconds.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- desktop, networking
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100