fix(server): rebind message-stream upstreams after a managed OpenCode restart
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
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
# 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
|
||||
|
||||
```sh
|
||||
# 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
|
||||
|
||||
1. A managed OpenCode process starts; the global message-stream hub connects to
|
||||
its `/global/event` SSE stream and chat events flow to the UI.
|
||||
2. The managed process "exits" but the actual server process survives on the
|
||||
old port (on Windows `killProcessOnPort` is a no-op and `taskkill` cannot
|
||||
reach the orphaned tree — the report shows leftover `opencode.exe serve`
|
||||
processes on historical ports).
|
||||
3. `restartOpenCode()` gives up after 5 s — logs
|
||||
`Timed out waiting for OpenCode port <old> to be released` — and spawns a
|
||||
fresh server on a NEW port, leaving the orphaned process running.
|
||||
4. HTTP/proxy traffic follows `state.openCodePort` to the new server, but the
|
||||
hub's upstream SSE reader stays pinned to the OLD server's `/global/event`
|
||||
stream (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 fake `opencode serve` binary 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.
|
||||
Reference in New Issue
Block a user