Skip to content

Commit 7a8dca4

Browse files
committed
Persist run.json's turn count at every turn boundary
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.
1 parent cad9a29 commit 7a8dca4

2 files changed

Lines changed: 27 additions & 1 deletion

File tree

src/tui/runner.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,18 @@ export function resolveExitCode(args: ResolveExitCodeArgs): number {
201201
return 0;
202202
}
203203

204+
// `inference.done` is the turn boundary every reactor cycle guarantees;
205+
// `reactor.done` fires once, at shutdown, and never between turns of a
206+
// long-lived interactive session. Keying the mid-run run.json snapshot off
207+
// `reactor.done` left turnsUsed frozen at its resume-time value for the
208+
// entire session — a live monorepo session showed turnsUsed: 0 with dozens
209+
// of turns already in the turns log. The terminal write on close still goes
210+
// through writeRunSnapshot directly with the real final status, so this
211+
// only needs to cover progress snapshots taken while the run is live.
212+
export function isRunSnapshotTurnBoundary(eventType: string): boolean {
213+
return eventType === "inference.done";
214+
}
215+
204216
/** One-line transcript block when resume history fails to load. */
205217
export function resumeTranscriptLoadErrorBlock(err: unknown): {
206218
type: "error";
@@ -1399,7 +1411,7 @@ export async function runTUI(initialConfig: Config): Promise<number> {
13991411
const streamSink = (event: Parameters<typeof runSink.sink>[0]): void => {
14001412
runSink.sink(event);
14011413
cycleRecorder.handleEvent(event);
1402-
if (event.type === "reactor.done") {
1414+
if (isRunSnapshotTurnBoundary(event.type)) {
14031415
void persistRunSnapshot("running");
14041416
}
14051417
};

tests/unit/tui/runner.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { EventEmitter } from "node:events";
33
import {
44
createTUIEventEmitter,
55
getTUIRunSummaryStatus,
6+
isRunSnapshotTurnBoundary,
67
loadLocalSettingsWriteBase,
78
resumeTranscriptLoadErrorBlock,
89
} from "../../../src/tui/runner.js";
@@ -21,6 +22,19 @@ test("createTUIEventEmitter can emit and receive events", () => {
2122
expect(received.length).toBe(1);
2223
});
2324

25+
// Regression: run.json's turnsUsed must update every turn, not only once
26+
// at reactor shutdown. reactor.done fires exactly once, at agent shutdown,
27+
// so a live multi-turn interactive session never had its progress snapshot
28+
// re-fire until close — turnsUsed sat frozen at its resume-time value the
29+
// whole session (CL-5534). inference.done is the turn boundary every
30+
// reactor cycle guarantees, so that's what a mid-run snapshot must key off.
31+
test("isRunSnapshotTurnBoundary fires on inference.done, not reactor.done", () => {
32+
expect(isRunSnapshotTurnBoundary("inference.done")).toBe(true);
33+
expect(isRunSnapshotTurnBoundary("reactor.done")).toBe(false);
34+
expect(isRunSnapshotTurnBoundary("reactor.error")).toBe(false);
35+
expect(isRunSnapshotTurnBoundary("connector.reply")).toBe(false);
36+
});
37+
2438
test("getTUIRunSummaryStatus distinguishes done, failed, and cancelled runs", () => {
2539
expect(getTUIRunSummaryStatus(true, undefined)).toBe("done");
2640
expect(getTUIRunSummaryStatus(true, "network failed")).toBe("failed");

0 commit comments

Comments
 (0)