You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(cli): TUI crashes with unhandled transport defect during post-update service restart #36688
During a routine auto-update service restart, every open TUI went to the "Connection lost / Reconnecting" screen, and one TUI crashed to the terminal with an unhandled transport defect. The root problem: the reconnect budget (3 attempts, ~16s) is much shorter than a realistic post-update election plus cold location boot (~45-60s observed), and when the budget is exhausted the ClientError escapes as a raw UnknownError stack dump instead of a clean exit.
ERROR (#1): UnknownError: An error occurred in Effect.tryPromise
at Tui.run (src/commands/handlers/default.ts:43:12)
at Tui.run (definition) (../tui/src/app.tsx:110:27) {
[cause]: ClientError: Transport
[cause]: Error: Unable to connect. Is the computer able to access the url?
}
Elected server (run 5170d17b, still v15422) auto-updates the package on disk to v15427 (updated OpenCode ... method=bun)
14:58:45
User starts a new TUI (oc, run fc781962, launcher already v15427)
14:58:46-47
Old server tears down (mass watcher stopped, then silence): version-mismatch restart
14:58:50-59:17
Election herd: ~10 serve --service contenders spawn from open TUIs
14:58:55-59:15
Contender fb876f01 wins, logs role=server, cold-boots ~10 location graphs over 5-15s, then at 14:59:15 tears down again, displaced without ever serving the client
14:59:01.331
The new TUI exhausts 3 reconnect attempts and crashes with the unhandled error above
14:59:31
Second-wave contender db40851e (PID 5056) wins, registers, and serves healthily
Problems
TUI reconnect gives up too early and crashes ungracefully. 3 attempts / ~16s loses the race against any post-update restart. When it does give up, the failure should be a friendly "server unavailable" message and clean exit, not an escaped defect through Effect.tryPromise in Tui.run (packages/cli/src/commands/handlers/default.ts:43, packages/tui/src/app.tsx:110).
Election double-bounce. The server restarted twice in one update cycle: 5170d17b -> fb876f01 (won, booted, torn down 20s later) -> db40851e. A second herd of contenders spawned while fb876f01 was still mid-boot and displaced it, doubling the outage window. Consistent with the known multi-holder service lock race.
Leaked contender process. A serve --service process from the previous day (PID 91121, ~1GB RSS, parented by a TUI) was still alive and unregistered long after losing its election. Losing contenders should exit.
Expected
An auto-update restart should be invisible or near-invisible to open clients: at most a brief reconnecting state, one server bounce, no crashes, no leaked processes.
I'd like to take this. The budget half is gone on v2 (indefinite retry), but the crash is still live — startup probes fail during a restart window and the ClientError escapes Tui.run unhandled. Plan: route it through the existing CLI error channel with a clean exit. Election double-bounce is server-side, out of scope.
PR #46726 is up with the fix — 4 required checks green. Only blocker: GitHub didn't pick up Fixes #36688 from the fork PR, so it needs a manual link in the PR's Development panel before it can merge. Everything else is ready for review. @kitlangton
Heads-up: the PR for this issue (#46726) was accidentally closed earlier today - its head fork got deleted unintentionally, which force-closed it. Re-filed as #48162 with the same commits. Sorry for the noise.
Windows / v2.0.23 datapoint: the benign branch of this same post-update race — one reconnect attempt, 6.8 s outage, no crash, no ERROR.
Mechanism (source verified against local clone, dev @ 907b3bc)
packages/client/src/generated/client.ts:167-168 — every fetch throw is wrapped:
}catch(cause){thrownewClientError("Transport",{ cause })
ClientError (packages/client/src/generated/client-error.ts) does super(reason, options), so message === "Transport" and the Bun/socket error rides in cause. Whatever formats the log line or the TUI toast emits Transport: <cause.message> — which is exactly the string I saw on screen:
Transport: The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the second argument to fetch()
The same text is logged verbatim by the event-stream client (17× in my log since 2026-09-28), and the wrap itself is visible in a logged ERROR:
timestamp=2026-10-01T04:13:01.219Z level=ERROR run=090ea0c4 message="unhandled rejection" cause="ClientError: Transport: The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the second argument to fetch() (cause: TypeError: The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the second argument to fetch())" role=cli
packages/cli/src/services/daemon.ts:110-135 — start() reuses a running server only on found?.version === InstallationVersion && compiled (line 114); otherwise stopProcess(found) (line 115) then spawn a new one. That is the version-mismatch restart, which my log records as reason=version-mismatch previousVersion=….
packages/tui/src/context/sync.tsx:451-547 — bootstrap()'s catch logs tui bootstrap failed (line 541) and then exit(e) when fatal (lines 546-547). That is the crash half of this issue. On my machine that path did not fire.
Timeline (verbatim from C:\Users\Lance\.local\share\opencode\log\opencode.log, UTC, 2026-10-05)
([…] = the multi-line stack frames of the InterruptError cause, elided; no other part of that line altered.)
Two supporting observations from the same window:
Run 60edadc9 (first serve --service spawn) emits 13 lines, the last at 08:38:37.377Z, then never logs again — no shutting down line — and it is not in the live process table. Run 92b823b7 (spawned 08:38:48.521Z) is the service that serves today (service.json pid 13344). Cause of the first spawn's disappearance: not determinable from the log.
Between 08:38:25Z and 08:40:46Z there are zerolevel=ERROR lines. The first ERROR after the update is at 08:40:47.458Z: 3× EPERM … rename 'session.json….tmp' (a separate mechanism — atomic-write rename collisions, not transport).
Expect vs Actual
Expect: an auto-update restart is invisible to open clients — at most a brief Reconnecting blip, no crash, no escaped ClientError defect.
Actual (this machine): that expectation held. Connect at 08:38:40.254Z, one failed attempt at 08:38:42.258Z, connected at 08:38:49.042Z → 6.78 s outage, 1 attempt, no tui bootstrap failed, no UnknownError stack, no ERROR line. The client-side screen text I saw at startup was Transport: The socket connection was closed unexpectedly… (see hygiene note below).
Repro (observed, not reproducible on demand)
Windows machine, opencode 2.0.22, shared managed service running.
Trigger the auto-updater (or start a CLI right after an update lands): log records updated OpenCode from=2.0.22 to=2.0.23 method=bun.
Start a TUI immediately after (cli starting version=2.0.23).
Client detects the registered server is 2.0.22 → background service starting reason=version-mismatch previousVersion=2.0.22 → old server tears down (InterruptError on /api/event) → new serve --service spawns.
During that window the TUI's event stream attempt fails (error="Timed out connecting to server", or on other runs error="Transport: The socket connection was closed unexpectedly…").
Observe: the retry succeeds ~7 s later, the TUI keeps running, and no ERROR is logged.
Counter-scenarios tried
#
Check (how)
Result
Alternative explanation it rules out
1
Every message="event stream disconnected" line was re-paired with the same run='s next event stream connected (log starts 2026-09-27T22:39:12.657Z; all counted events are 2026-09-28 or later)
33 disconnects, all level=INFO; 31 reconnected within 0.1–9.3 s; 1 (d6f9fff6, 09-28T06:40:17Z) re-disconnected 7 s later and then reconnected 9.3 s after that; 1 (090ea0c4, 10-01T05:41:22Z) has no reconnect because that line is the run's last log line — process exit
"The Transport text is a fatal/crash signature" — it never is here; the SSE retry-with-backoff path absorbs it every time
3 processes only: 19636 .bun\bin\opencode.exe (08:38:36Z), 21136 its child, no args (08:38:36Z, = run 6d168471, confirmed by the session.json.21136.*.tmp names in the 08:40:47 EPERM lines), 13344 …\@opencode\cli\bin\opencode.exe serve --service (08:38:48Z, = service.json pid)
"A lingering 2.0.22 daemon from before the update caused the failure" — nothing older than 08:38:36Z is alive, i.e. everything started after the 08:38:25Z update line
3
~/.local/state/opencode/service.json
exactly one registration: {"id":"45b432f6-…","version":"2.0.23","url":"http://127.0.0.1:49374","pid":13344,"password":"<redacted>"}
"leftover atomic-write .tmp litter from the EPERM renames is still around"
5
level=ERROR between 08:38:25Z and 08:40:46Z
0 lines (first ERROR after the update is the 3× EPERM block at 08:40:47.458-459Z)
"the TUI crashed / bootstrap() took the exit(e) path at this update" — corroborated by 0 genuine hits for tui bootstrap failed and UnknownError as application log lines (the only whole-log matches are our own grep commands echoed back by the server)
Evidence hygiene (what I am not claiming)
The literal string Transport: The socket connection was closed unexpectedly… does not appear in opencode.log on 2026-10-05. The log's record of that startup failure reads error="Timed out connecting to server" (quoted above); the on-screen Transport: … text is user-observed in the TUI, and its provenance from a specific call at 08:38Z is UNVERIFIED. What is verified: that exact string is logged 17× as an event-stream error= value, and once as a ClientError: Transport: … (cause: TypeError: …) unhandled rejection in a role=cli process.
Log emitters for event stream disconnected and background service starting … reason=version-mismatch were not located in my local clone (dev @ 907b3bc) — the strings are verified from the shipped 2.0.23 binary's output only. The version-gated restart logic they describe is verified at daemon.ts:110-135.
Environment
Caption : Microsoft Windows 10 Pro
Version : 10.0.19045
BuildNumber : 19045
OSArchitecture : 64-bit
opencode v2.0.23
bun 1.4.2
gh version 2.102.0 (2026-09-30)
git version 2.55.0.windows.5
Why this helps this issue
This is the good outcome of the same race this issue reports the bad outcome of: same trigger (version-mismatch restart after auto-update), same ClientError("Transport") surface, and on this machine the reconnect budget was never exhausted, so bootstrap() never reached its exit(e). It brackets severity for #36688: the failure is not "Windows always crashes", it is "whoever loses the ~7 s race crashes — and this run won". It also shows the same double-spawn shape as the issue's problem #2 (two serve --service spawns 11.8 s apart in one update cycle, the first one's log trail ending with no shutdown line).
Seen on Windows 11 (TUI 2.0.27, Desktop app 2.0.32). Trigger here is the version-mismatch replace from #55429: the TUI starts its own service while the Desktop app restarts its own, the TUI's startup probe fails, and it exits with the same unhandled error:
cli process failed cause=... UnknownError: An error occurred in Effect.tryPromise (cause: ClientError: Transport: Unable to connect. Is the computer able to access the url?)
The fix PR #48162 was auto-closed on 9 Oct without a review. Could it be reopened?
Activity
holny commentedon Sep 2, 2026
I'd like to take this. The budget half is gone on
v2(indefinite retry), but the crash is still live — startup probes fail during a restart window and the ClientError escapesTui.rununhandled. Plan: route it through the existing CLI error channel with a clean exit. Election double-bounce is server-side, out of scope.holny commentedon Sep 2, 2026
PR #46726 is up with the fix — 4 required checks green. Only blocker: GitHub didn't pick up
Fixes #36688from the fork PR, so it needs a manual link in the PR's Development panel before it can merge. Everything else is ready for review. @kitlangtonholny commentedon Sep 9, 2026
Heads-up: the PR for this issue (#46726) was accidentally closed earlier today - its head fork got deleted unintentionally, which force-closed it. Re-filed as #48162 with the same commits. Sorry for the noise.
Bearmancer commentedon Oct 5, 2026
Windows / v2.0.23 datapoint: the benign branch of this same post-update race — one reconnect attempt, 6.8 s outage, no crash, no ERROR.
Mechanism (source verified against local clone,
dev@907b3bc)packages/client/src/generated/client.ts:167-168— every fetch throw is wrapped:ClientError(packages/client/src/generated/client-error.ts) doessuper(reason, options), somessage === "Transport"and the Bun/socket error rides incause. Whatever formats the log line or the TUI toast emitsTransport: <cause.message>— which is exactly the string I saw on screen:packages/cli/src/services/daemon.ts:110-135—start()reuses a running server only onfound?.version === InstallationVersion && compiled(line 114); otherwisestopProcess(found)(line 115) then spawn a new one. That is the version-mismatch restart, which my log records asreason=version-mismatch previousVersion=….packages/tui/src/context/sync.tsx:451-547—bootstrap()'s catch logstui bootstrap failed(line 541) and thenexit(e)whenfatal(lines 546-547). That is the crash half of this issue. On my machine that path did not fire.Timeline (verbatim from
C:\Users\Lance\.local\share\opencode\log\opencode.log, UTC, 2026-10-05)(
[…]= the multi-line stack frames of theInterruptErrorcause, elided; no other part of that line altered.)Two supporting observations from the same window:
60edadc9(firstserve --servicespawn) emits 13 lines, the last at08:38:37.377Z, then never logs again — noshutting downline — and it is not in the live process table. Run92b823b7(spawned08:38:48.521Z) is the service that serves today (service.jsonpid 13344). Cause of the first spawn's disappearance: not determinable from the log.08:38:25Zand08:40:46Zthere are zerolevel=ERRORlines. The first ERROR after the update is at08:40:47.458Z: 3×EPERM … rename 'session.json….tmp'(a separate mechanism — atomic-write rename collisions, not transport).Expect vs Actual
Reconnectingblip, no crash, no escapedClientErrordefect.08:38:40.254Z, one failed attempt at08:38:42.258Z, connected at08:38:49.042Z→ 6.78 s outage, 1 attempt, notui bootstrap failed, noUnknownErrorstack, no ERROR line. The client-side screen text I saw at startup wasTransport: The socket connection was closed unexpectedly…(see hygiene note below).Repro (observed, not reproducible on demand)
updated OpenCode from=2.0.22 to=2.0.23 method=bun.cli starting version=2.0.23).background service starting reason=version-mismatch previousVersion=2.0.22→ old server tears down (InterruptErroron/api/event) → newserve --servicespawns.error="Timed out connecting to server", or on other runserror="Transport: The socket connection was closed unexpectedly…").Counter-scenarios tried
message="event stream disconnected"line was re-paired with the samerun='s nextevent stream connected(log starts 2026-09-27T22:39:12.657Z; all counted events are 2026-09-28 or later)level=INFO; 31 reconnected within 0.1–9.3 s; 1 (d6f9fff6, 09-28T06:40:17Z) re-disconnected 7 s later and then reconnected 9.3 s after that; 1 (090ea0c4, 10-01T05:41:22Z) has no reconnect because that line is the run's last log line — process exitGet-CimInstance Win32_Process -Filter "Name='opencode.exe'"(PID, PPID, CreationDate, CommandLine).bun\bin\opencode.exe(08:38:36Z), 21136 its child, no args (08:38:36Z, = run6d168471, confirmed by thesession.json.21136.*.tmpnames in the 08:40:47 EPERM lines), 13344…\@opencode\cli\bin\opencode.exe serve --service(08:38:48Z, =service.jsonpid)~/.local/state/opencode/service.json{"id":"45b432f6-…","version":"2.0.23","url":"http://127.0.0.1:49374","pid":13344,"password":"<redacted>"}Get-ChildItem ~\.local\state\opencode -Filter *.tmp -Recurse.tmplitter from the EPERM renames is still around"level=ERRORbetween08:38:25Zand08:40:46Z08:40:47.458-459Z)bootstrap()took theexit(e)path at this update" — corroborated by 0 genuine hits fortui bootstrap failedandUnknownErroras application log lines (the only whole-log matches are our own grep commands echoed back by the server)Evidence hygiene (what I am not claiming)
Transport: The socket connection was closed unexpectedly…does not appear inopencode.logon 2026-10-05. The log's record of that startup failure readserror="Timed out connecting to server"(quoted above); the on-screenTransport: …text is user-observed in the TUI, and its provenance from a specific call at 08:38Z is UNVERIFIED. What is verified: that exact string is logged 17× as an event-streamerror=value, and once as aClientError: Transport: … (cause: TypeError: …)unhandled rejection in arole=cliprocess.event stream disconnectedandbackground service starting … reason=version-mismatchwere not located in my local clone (dev@907b3bc) — the strings are verified from the shipped 2.0.23 binary's output only. The version-gated restart logic they describe is verified atdaemon.ts:110-135.Environment
Why this helps this issue
This is the good outcome of the same race this issue reports the bad outcome of: same trigger (version-mismatch restart after auto-update), same
ClientError("Transport")surface, and on this machine the reconnect budget was never exhausted, sobootstrap()never reached itsexit(e). It brackets severity for #36688: the failure is not "Windows always crashes", it is "whoever loses the ~7 s race crashes — and this run won". It also shows the same double-spawn shape as the issue's problem #2 (twoserve --servicespawns 11.8 s apart in one update cycle, the first one's log trail ending with no shutdown line).vladkosinov commentedon Oct 26, 2026
Seen on Windows 11 (TUI 2.0.27, Desktop app 2.0.32). Trigger here is the version-mismatch replace from #55429: the TUI starts its own service while the Desktop app restarts its own, the TUI's startup probe fails, and it exits with the same unhandled error:
The fix PR #48162 was auto-closed on 9 Oct without a review. Could it be reopened?