Repository navigation
fix(opencode): close the native event stream when a snapshot shows its last pane stopped being OpenCode - #2389
Conversation
…s last pane stopped being OpenCode Native OpenCode ownership (verifiedPanes and the shared event stream to opencode serve) was released only by a status event naming another agent and by pane end. The collector stores a first-seen pane as a baseline without a status event, replays a gap by status rather than agent, and recovers through resync, so a pane that stopped being OpenCode where the bridge learned it only from a snapshot kept the stream open for good. This was the cause of the opencode contract flake "closes the shared stream when its last verified pane ends or stops being OpenCode". Reconciled snapshots now release a verified pane when they name another agent or a known status without one. An unnamed "unknown" pane (a server handover) is kept unless process info proves the shell alone is in front, by its name and argv, not by pid: exec opencode keeps the shell's pid. A verification generation read before each snapshot keeps ownership verified since then, and only verified panes are released, so an older snapshot cancels neither a newer verification nor one still in flight. Replay releases go through the same guarded path. Regression cases with the real collector: 21 passed and 6 failed before the guards, 28 pass after (the in-flight case fails without its guard); related suites 149/0; the opencode contract file passed 24/24 three times; bun run check fast ok.
📝 Summary
Merge Risk:
|
…hrough that snapshot's release The checkpoint read before a collector snapshot spared only verifications that had completed since; one still in flight was cancelled by the older snapshot's release, its successful session lookup was discarded, and the shared stream closed under a pane that was still OpenCode. Stamp each verification when it starts from the same counter, and let a release with a checkpoint stand down while the pane has one begun after it: that newer attempt settles the pane itself (it releases on its own if the session is gone). Releases without a checkpoint (status events, pane end) still cancel everything in flight. Regression with prior ownership (server/opencode-native.test.ts): the re-verification resolved null and the stream closed before the fix (1 fail), and stands after it. Related suites 150/0; opencode contract 24/24 three times; bun run check fast ok.
|
Merging 4620a38. Native OpenCode ownership and the shared event stream to opencode serve were released only when the status named another agent or the pane ended, so a pane that stopped being OpenCode between snapshots could keep the stream open (the flaky contract case 'closes the shared stream when its last verified pane ends or stops being OpenCode'). A status snapshot that shows the last verified pane is no longer OpenCode now releases it too; a verification that completed, or began, after the snapshot was read is newer evidence and is not cancelled by it. Review by the merge pass: one reproduced MEDIUM (an older snapshot cancelling a newer in-flight re-verification and closing the stream under a pane still OpenCode) fixed in 4620a38 with a regression test; 149 unit tests, typecheck and the contract case 20/20 passed on the first round. |
Found by the flake-hunt lane: Integration failed once on 2026-10-28 12:54Z (job 122825435909), after #2223, at
server/opencode.contract.test.ts"OpenCode v2 native service > closes the shared stream when its last verified pane ends or stops being OpenCode". The error was "the stream closing after the last verified pane stopped being OpenCode did not happen (native {"stream":true,"panes":["w8:p1"]})". A rerun passed.Cause (product race)
Native OpenCode ownership (
verifiedPanesinserver/opencode-native.ts, and the shared event stream toopencode serve) was released only byonStatusnaming another agent and by pane end (server/core/status.ts). The status collector (server/collector.ts) does not always callonStatus:onResync.So when a pane stopped being OpenCode and the bridge learned it only from a snapshot, the stream stayed open with nothing left to release it: an event stream to
opencode serveheld for good. The CI failure is one such ordering.Reproduced: with the real collector and the status hub's callbacks (
server/core/status.test.ts, "native OpenCode ownership reconciliation"), a first baseline, an equal-status gap and a resync that each show the pane no longer OpenCode all leave{ stream: true }on origin/main: 6 cases fail.Change (
server/core/status.ts,server/opencode-native.ts)unknown, as during a server handover, is kept. The exception is whenpane.process_infoproves the shell alone is in front: one foreground process that is the shell by name and argv, not just by pid, sinceexec opencodekeeps the shell's pid. That read happens only for verified panes in that state, and not on Windows, which reports only the shell.Proof
server/core/status.test.ts,server/collector.test.tsandserver/opencode-native.test.tspass 149/0.bun run check run bun test --timeout 30000 ./server/opencode.contract.test.tspassed 24/24, three times in a row.bun run check fastok.Reviews
exec opencodethat released a live pane.Known limits (LOW, not fixed here)
unknownstill releases without shell proof, as before this change.Second commit: a re-verification begun after a snapshot's read
The merge pass blocked 0c9ceca on one reproduced MEDIUM: an older status snapshot could cancel a newer in-flight re-verification of an already-verified pane. The checkpoint guarded only completed verifications, so the successful session lookup was discarded and the shared stream closed under a pane that was still OpenCode.
Change: each verification is stamped when it starts, from the same counter.
releaseOpenCodePane(pane, checkpoint)stands down while the pane has a verification begun after that checkpoint; the newer attempt settles the pane itself. Releases without a checkpoint (status events, pane end) still cancel everything in flight.Proof:
server/opencode-native.test.tsfailed before the fix (the re-verification resolvednulland the stream closed) and passes after.bun run check fastok.Reviews: