When the managed OpenCode process exits but a server survives on the old port (Windows: killProcessOnPort is a no-op, so the orphaned process tree keeps the port), restartOpenCode() times out waiting for the port and spawns a fresh server on a NEW port. HTTP/proxy traffic follows the new port, but the global message-stream hub's upstream SSE reader stays pinned to the old server's /global/event stream — that connection never closes — so new events never reach the UI and the chat stops updating until the app is restarted (#2638). Lifecycle now fires an optional onOpenCodeRestarted hook after a successful managed restart; index.js wires it to the new messageStreamRuntime.rebindUpstream(), which restarts the shared hub (its reader re-dials buildOpenCodeUrl → the current port) and closes directory-scoped sockets so their per-connection readers rebuild against the new port. External servers are untouched (their port cannot change). Fixes #2638
2.2 KiB
2.2 KiB
Reproduction: Chat UI stops updating until the desktop app is restarted (#2638)
Reproduces https://github.com/openchamber/openchamber/issues/2638 using the
real server modules (lifecycle.js, global-hub.js, network-runtime.js),
real processes, and real ports — no mocks.
Run
# Windows-orphan scenario (the reported bug):
node scripts/repro/issue-2638/reproduce-2638.mjs
# Control: healthy restart on Linux (hub reconnects, UI keeps updating):
node scripts/repro/issue-2638/reproduce-2638.mjs --baseline
Requires lsof (used only for cleanup). The default run simulates Windows
(process.platform is temporarily overridden to win32) because the bug is
specific to the Windows process-lifecycle path.
What it demonstrates
- A managed OpenCode process starts; the global message-stream hub connects to
its
/global/eventSSE stream and chat events flow to the UI. - The managed process "exits" but the actual server process survives on the
old port (on Windows
killProcessOnPortis a no-op andtaskkillcannot reach the orphaned tree — the report shows leftoveropencode.exe serveprocesses on historical ports). restartOpenCode()gives up after 5 s — logsTimed out waiting for OpenCode port <old> to be released— and spawns a fresh server on a NEW port, leaving the orphaned process running.- HTTP/proxy traffic follows
state.openCodePortto the new server, but the hub's upstream SSE reader stays pinned to the OLD server's/global/eventstream (that connection never closed), so events emitted by the new server never reach the UI: the chat UI goes stale while the new server keeps persisting session data — visible only after restarting the app.
The --baseline control proves the reconnect logic itself is fine: when the
old process dies and the port is properly released, the hub reconnects to the
new port and events are delivered.
Files
reproduce-2638.mjs— the reproduction driver (assertions + summary).fake-opencode-serve.mjs— a fakeopencode servebinary whose launcher spawns a detached server core that survives the launcher's death (Windows-style orphan), plus an in-process mode for the baseline control.