Sync run.json turn count and idle the shell at every turn boundary - #368
Conversation
Independent review — ready to mergeVerified by running, not by reading. Left open for human review. CL-5534's fix is real. Drove CL-5570 was independently confirmed already fixed. The new two-turn test passes on unmodified Full
Typecheck, build clean. 4039 pass / 0 fail, and identical under Two non-blocking notes for the human reviewer
Also suggested: an end-to-end test driving the real modules to disk would be stronger than the current unit test, which only exercises the predicate in isolation. |
13f29e2 to
196344c
Compare
|
Parking for the release candidate. Typecheck is currently broken on this branch — |
runtime-bridge.ts already keys the shell's run-idle transition off inference.done (the turn boundary), not reactor.done (shutdown, fires once) -- fixed alongside the queued-message drain in an earlier commit. That left the multi-turn case unverified: a test only proved a single turn settled run back to idle, not that a second turn starts from idle again after the first one closes it out. Add a regression test that drives two consecutive turns end to end and asserts run returns to idle, and that a message submitted between them sends immediately rather than routing through the queue.
The mid-run progress snapshot in the TUI runner only re-fired on reactor.done, which fires exactly once, at agent shutdown, and never between turns of a long-lived interactive session. A live monorepo session showed run.json stuck at turnsUsed: 0 and status: running for its entire multi-turn lifetime, with dozens of completed turns already in context/turns.jsonl -- resume pickers and anything else trusting run.json had no truthful signal until the process closed. inference.done is the turn boundary every reactor cycle guarantees (the same one the shell's run-idle transition keys off), so key the snapshot write off that instead. The terminal write on clean exit and the crash path both already write through directly with the final status, so this only changes progress snapshots taken while the run is still live.
isRunSnapshotTurnBoundary had exactly one production caller in the same file, so its export existed only for a unit test. Make it module-private and replace that test with an end-to-end one that drives createRunSink, saveState, and loadState against a real temp session directory: turnsUsed incrementing per turn as read back off disk, 20 rapid back-to-back turns with no settling delay, and a late running write racing a done write to confirm status never resurrects.
run.json is session domain state, and src/session/run-sink.ts already owns the turn count and already special-cases inference.done there (clearing a stale runError). The mid-run snapshot trigger lived in src/tui/runner.ts instead, keyed off a second subscription to the same event stream — the exact shape that has already cost this constraint three renderer swaps. Move the cadence into run-sink via an onTurnBoundarySnapshot callback, so the renderer only owns how to persist a snapshot, not when one is due. Named apart from the onTurnBoundary predicate in src/agent/reactor-events.ts, which run-sink now uses for the same inference.done check it already made inline. The end-to-end test now drives the callback the way production wiring does, instead of calling saveState directly after each sink call, so it actually exercises the trigger rather than simulating it.
22353ee to
b0493e6
Compare
Summary
Two run-state fixes, both about
inference.donebeing the turn boundary versusreactor.donebeing reactor shutdown (fires once, never between turns).Shell idle between turns.
shell.session.runreturning to idle between turns was already fixed onmain(keyed offinference.doneinsrc/tui-opentui/runtime-bridge.ts), but had no test proving it across more than one turn. Added a regression test insrc/tui-opentui/runtime-bridge.test.tsthat drives two consecutive turns and assertsrunreturns to idle between them, and that a message typed in between sends immediately instead of queuing.run.jsonturn count staying in sync. The mid-run progress snapshot only re-fired onreactor.done(shutdown), so a long interactive session never re-persisted its turn count after the initial resume-time write —turnsUsedsat frozen whileturns.jsonlkept growing. Rekeyed the snapshot trigger toinference.done, and moved the trigger itself intosrc/session/run-sink.ts(which already owns the turn count and already special-casesinference.donethere) via anonTurnBoundarySnapshotcallback, instead of a second subscription to the same event stream in the renderer.Rebase note
This branch sat stale since 2026-08-07 while
origin/mainmoved substantially. Today's main independently added a sharedonTurnBoundarytype-guard predicate insrc/agent/reactor-events.tsand wiredsrc/session/run-sink.tsto use it for the sameinference.donecheck this branch was making inline. Rebasing produced a silent naming collision: this branch's ownonTurnBoundarycallback field onRunSinkArgsshadowed the imported predicate, soif (onTurnBoundary(event))tried to invoke the optional callback with an argument it doesn't accept — that's the TS2722/TS18048/TS1345/TS2554 failure CI was reporting atsrc/session/run-sink.ts:127. Fixed by renaming this branch's callback toonTurnBoundarySnapshotthroughout, keeping main'sonTurnBoundarypredicate for the boundary check.Scope notes
src/exec/runner.tsbuilds its owncreateRunSink(...)(src/exec/runner.ts:557) and does not passonTurnBoundarySnapshot— exec is a single-shot run that persists state explicitly via its ownpersist()helper, not the long-lived interactive session this fix targets, so it's unaffected either way.listSessions(src/session/index.ts:251-256) still fabricatesstatus: "running"forever for a session directory that hascontext/but crashed before its firstrun.jsonwrite. This change writesrun.jsonsooner (at the first turn boundary instead of only at start/end), which narrows that window, but doesn't close it for a session that crashes before completing even one turn. Not fixed here — it's a separate, pre-existing gap.Verification
bun run typecheck— cleanbun run build— cleanbun run test(./src ./tests ./evals) — 4139 pass, 0 fail