review phase 1, and report the bug the live panel made load-bearing
Phase 1 accepted and phase 2 answered well. One real defect: the supersede path in
runOpenCodeTurn kills a stale turn without marking it, so the dead process late-fires
finish() against the turn that replaced it — committing a false "OpenCode exited" to
chat_session_events, deleting the live handle from `running`, killing the stop button
and orphaning the process.
Predates this pass; reported now because e8bd946 is what made the map load-bearing.
Reproduced with a stub binary rather than argued — the transcript is in the doc.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
# Phase 1 review, and one bug the Live panel work made load-bearing
|
||||
|
||||
Review of `cfbf58c`, `d7b2231`, `8b409e8`, `e8bd946`, `1402880` against `docs/opencode-parity.md` and
|
||||
`docs/opencode-phase1-report.md`.
|
||||
|
||||
**Verdict: Phase 1 accepted. Phase 2's answer accepted, and the recommendation is the right one.** One
|
||||
real defect, reproduced below, plus two comments that describe behaviour the code does not have.
|
||||
|
||||
---
|
||||
|
||||
## Accepted, and why
|
||||
|
||||
**The `AGENTS.md` deletion is complete.** Not just the seeding code — the file itself is gone from
|
||||
`DATA_PATH/opencode_server/` on alpha, which the code could not have done and which a lesser pass would
|
||||
have left behind for the next reader to find and wonder about.
|
||||
|
||||
**The dead serve-turn deletion is the model for how to delete things.** `5d077a4` recorded as the last
|
||||
commit where the path was live, `isServerHealthy`'s removal annotated with what it did, that it *worked*,
|
||||
and which route to prefer if a health check is ever wanted back. Nothing here needs archaeology later.
|
||||
|
||||
**`mapRunLine` is a faithful extraction.** Checked branch by branch against the original `handleLine`,
|
||||
including the two easy things to get wrong: cost accrues only on `step_finish` (every other line returns
|
||||
a zero delta, so unconditional accumulation is equivalent), and the early `tool_use` bail on a missing
|
||||
`callID` still contributes nothing. The 11 tests are real ones — NaN-vs-missing-tokens, empty text parts,
|
||||
non-JSON interleaving, an unknown future event type.
|
||||
|
||||
**Phase 2's blocker is genuinely answered**, and the trap is the valuable half: the body form of
|
||||
`directory` being silently ignored yields a session in the serve's own cwd, which reads exactly like "the
|
||||
serve cannot do per-request directories". That is a wrong answer anyone re-checking this would have
|
||||
reached too. Recording it cost a line and saves the next person a day.
|
||||
|
||||
**Verified independently:** `tsgo` clean. 573 tests, 571 passing — the two failures are host-local
|
||||
(`cliamp not found on host`, and the pty test timing out waiting for a real shell), unrelated to this
|
||||
work.
|
||||
|
||||
---
|
||||
|
||||
## The defect: a superseded turn takes the live turn down with it
|
||||
|
||||
`runOpenCodeTurn` supersedes a lingering turn for the same `sessionKey` (`runner.ts:59-67`) by killing the
|
||||
process and dropping it from `running`. It does not mark the handle. So when that process actually dies,
|
||||
its own `proc.exited` closure still runs — with `done === false` and `killedByUser === false` — and calls
|
||||
`finish(...)`, which now acts on **the turn that replaced it**.
|
||||
|
||||
Reproduced with a stub binary in place of `opencode` (a script that sleeps), two turns on one
|
||||
`sessionKey`:
|
||||
|
||||
```
|
||||
--- turn 1 starts ---
|
||||
running: [{"sessionKey":"sess-A"}]
|
||||
--- turn 2 supersedes it ---
|
||||
running immediately after: [{"sessionKey":"sess-A"}]
|
||||
EMIT: {"type":"opencode:event","sessionKey":"sess-A",
|
||||
"event":{"type":"error","message":"OpenCode exited with code 143"}}
|
||||
--- after turn 1 is reaped, turn 2 still generating ---
|
||||
running: []
|
||||
stop button reached a process? false
|
||||
orphan still alive: /bin/sh fake-opencode run --format json … --dir /tmp/oc-probe two
|
||||
```
|
||||
|
||||
Four consequences, worst first:
|
||||
|
||||
1. **A false error is committed to the transcript.** The emit goes through
|
||||
`sessionLog.push(sessionKey, event, durable)` in `index.ts:139-143`, so `OpenCode exited with code 143`
|
||||
lands in `chat_session_events` against a session that is generating normally, and replays on every
|
||||
reload. This is not a transient UI artefact.
|
||||
2. **`listRunningOpenCodeTurns` goes blind.** `finish` calls `running.delete(sessionKey)` unconditionally,
|
||||
removing the *new* handle. The Live panel added in `e8bd946` therefore omits exactly the turn it
|
||||
exists to show.
|
||||
3. **The stop button dies.** `killOpenCodeTurn` finds nothing in `running` and returns silently.
|
||||
4. **The process orphans.** No handle means the next turn's supersede cannot kill it either.
|
||||
|
||||
The bug predates this pass — it is a property of the supersede path, not of anything in these commits.
|
||||
It is reported here because `e8bd946` is what made `running`'s accuracy load-bearing: before the Live
|
||||
panel, entries 2 and 3 were invisible.
|
||||
|
||||
**Suggested fix**, both halves needed:
|
||||
|
||||
- Mark the handle in the supersede branch (`stale.superseded = true`), and have `proc.exited` return
|
||||
early on it — no `finish`, no emit, no delete. A turn the system replaced on purpose is not an error.
|
||||
- Make the delete identity-checked regardless: `if (running.get(sessionKey) === handle) running.delete(...)`.
|
||||
Cheap, and it closes the whole family rather than this one path.
|
||||
|
||||
**Worth reproducing before fixing**, in your own words back to me: the stub-binary trick above is enough,
|
||||
no real `opencode` needed. `RunnerConfig.bin` is the only injection point required.
|
||||
|
||||
### The related gap
|
||||
|
||||
The extraction moved the pure mapping under test and left the caller holding the two line-spanning
|
||||
concerns — emit-the-session-id-once, and cost accumulation. That split is correct. But those two are now
|
||||
the *only* untested logic on the path, and this defect lives in that same untested caller. Worth one test
|
||||
over `runOpenCodeTurn`'s lifecycle (stub binary, two turns, assert `running` and the emitted events)
|
||||
rather than more tests over `mapRunLine`, which is well covered.
|
||||
|
||||
---
|
||||
|
||||
## Two comments that outrun the code
|
||||
|
||||
**1. `/chat/live` describes a lookup that does not happen** (`chat.ts`, the OpenCode block). The comment
|
||||
says title and cwd "come from the session store, which is keyed on the `ses_…` id the runner reports …
|
||||
so a turn whose id has not been reported yet shows unnamed rather than guessing." Nothing is looked up —
|
||||
both fields are literal `null`, and `LiveOpenCodeSession` carries only `sessionKey`, so there is no id to
|
||||
look one up with. They are null permanently, not until-reported.
|
||||
|
||||
The report repeats this as "title/cwd are null until the `ses_…` id is reported". No mechanism exists to
|
||||
change them. Either say so plainly, or plumb the id into `LiveOpenCodeSession` and do the lookup — but
|
||||
the comment should not describe the second while the code does the first.
|
||||
|
||||
**2. `protocol.ts` — the new type landed inside another type's docblock.** `LiveOpenCodeSession` was
|
||||
inserted between `LiveClaudeSession`'s comment and `LiveClaudeSession` itself. That comment (about
|
||||
`isGenerating`, `pendingTasks`, and what the idle GC consults) now reads as documentation for the
|
||||
OpenCode type, where it is not merely wrong but directly contradicted by the correct comment immediately
|
||||
below it. Cosmetic anywhere else; this file is the wire contract between the server and every sidecar.
|
||||
|
||||
---
|
||||
|
||||
## On `opencode:list` being unexercised
|
||||
|
||||
Your own flag — it has never returned a non-empty list — is the right thing to have flagged, and it
|
||||
compounds with the defect above: the first time it *is* exercised with two messages on one session, it
|
||||
will return `[]` for a turn that is plainly running, and that will look like the enumeration being broken
|
||||
rather than the supersede path deleting the handle. Fix the supersede first, then exercise it; otherwise
|
||||
the five minutes of testing produces a misleading result.
|
||||
|
||||
---
|
||||
|
||||
## Agreed next, unchanged from your list
|
||||
|
||||
Your four suggestions are the right four. Ordering note only: **item 4 (B7's latent spurious `cut-off`)
|
||||
before the fork decision**, for the reason you gave yourself — your B2 fix deepens the mask, and a latent
|
||||
bug that is getting better hidden is the one to take while it is still findable.
|
||||
|
||||
The fork itself: agreed, do not start it, and agreed the third option is the one to take if it is taken.
|
||||
Andre makes that call, not either of us.
|
||||
Reference in New Issue
Block a user