Skip to content

Commit ad179a9

Browse files
committed
Seed turnsUsed and mcpServers from the resumed session's run.json
Resuming a session unconditionally reset the run sink's turn counter to zero and connectedMcpServers to empty, both in memory and by immediately persisting those zeroed values to run.json before the reactor even started. Seed createRunSink's collector and the connectedMcpServers list from the picked session's prior state so resume continues the count instead of restarting it.
1 parent 1970470 commit ad179a9

4 files changed

Lines changed: 40 additions & 7 deletions

File tree

‎src/session/hooks.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,9 @@ export type TurnContextCollectorOptions = {
174174
// standing copy of recent history, so callers with nothing to hand it to
175175
// (no lifecycle hook) can opt out of retaining it.
176176
retainHistory?: boolean;
177+
// Resuming a session should continue the persisted run.json turn count
178+
// rather than restart it at zero.
179+
initialTurnCount?: number;
177180
};
178181

179182
export function createTurnContextCollector(
@@ -193,7 +196,7 @@ export function createTurnContextCollector(
193196
} {
194197
const retainHistory = options.retainHistory ?? true;
195198
const turns: TurnContext[] = [];
196-
let turnCount = 0;
199+
let turnCount = options.initialTurnCount ?? 0;
197200
let pending: PendingTurn | null = null;
198201
let cycleStartedAt = now();
199202
let tokenUsage: TokenUsage = { ...emptyUsage };

‎src/session/run-sink.test.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,24 @@ describe("createRunSink", () => {
7777
expect(runSink.getTokenUsage()).toEqual({ input: 1, output: 1, cacheRead: 0, cacheWrite: 0, thinking: 0 });
7878
});
7979

80+
test("seeds the turn count from a resumed session's prior turnsUsed", () => {
81+
const runSink = createRunSink({
82+
emitter: new EventEmitter(),
83+
hookManager: stubHookManager([]),
84+
initialTurnCount: 7,
85+
});
86+
87+
expect(runSink.getTurnCount()).toBe(7);
88+
89+
runSink.sink(event("inference.done", {
90+
turn: { role: "assistant", content: [], model: "test", timestamp: 0 },
91+
usage: { input: 1, output: 1, cacheRead: 0, cacheWrite: 0, thinking: 0 },
92+
source: { provider: "test", model: "test" },
93+
}));
94+
95+
expect(runSink.getTurnCount()).toBe(8);
96+
});
97+
8098
test("getLastTurnUsage reports the latest turn alone, not the running sum", () => {
8199
const runSink = createRunSink({
82100
emitter: new EventEmitter(),

‎src/session/run-sink.ts‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ export type RunSinkArgs = {
1919
// turn actually ran against, so consumers report per-turn provider/model
2020
// even if the live selection changed mid-run.
2121
onTurnComplete?: (ctx: import("./hooks.js").TurnContext) => void;
22+
// Continues a resumed session's persisted run.json turn count instead of
23+
// restarting the collector at zero.
24+
initialTurnCount?: number;
2225
};
2326

2427
export type RunSink = {
@@ -66,7 +69,7 @@ export function resolveExecRunStatus(args: {
6669
}
6770

6871
export function createRunSink(args: RunSinkArgs): RunSink {
69-
const { emitter, hookManager, onTurnComplete } = args;
72+
const { emitter, hookManager, onTurnComplete, initialTurnCount } = args;
7073

7174
function hasConfiguredHooks(): boolean {
7275
return hookManager.getStatuses().length > 0;
@@ -82,15 +85,19 @@ export function createRunSink(args: RunSinkArgs): RunSink {
8285
onTurnComplete?.(ctx);
8386
};
8487

85-
function createCollector(): TurnCollector {
88+
// The initial seed only applies to the run's first collector (a resumed
89+
// session's prior turnsUsed); a later reset() starts a fresh sub-session
90+
// and should count from zero, not re-seed.
91+
function createCollector(seedTurnCount?: number): TurnCollector {
8692
return createTurnContextCollector(handleTurn, Date.now, {
8793
retainHistory: hasConfiguredHooks(),
94+
...(seedTurnCount !== undefined ? { initialTurnCount: seedTurnCount } : {}),
8895
});
8996
}
9097

9198
let runCompleted = false;
9299
let runError: string | undefined;
93-
let turnCollector = createCollector();
100+
let turnCollector = createCollector(initialTurnCount);
94101
// Always-on local PerfTrace: not gated by lifecycle hooks.
95102
let perfObserver = createPerfReactorObserver();
96103

‎src/tui/runner.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -406,13 +406,17 @@ export async function runTUI(initialConfig: Config): Promise<number> {
406406
let resumeSkipInitialTask = config.skipInitialTask === true;
407407
let startedAt = Date.now();
408408
let runTaskTitle = config.task;
409+
// Carries the resumed session's prior run.json forward so turnsUsed and
410+
// mcpServers can be seeded instead of silently reset to zero/empty.
411+
let resumedState: RunState | null = null;
409412

410413
if (config.resumePicker) {
411414
const picked = await pickSession(config.cwd, { includeCompleted: config.force });
412415
if (picked === null) return 0;
413416
sessionId = picked.sessionId;
414417
resumeSkipInitialTask = true;
415418
const pickedState = await loadState(config.cwd, sessionId);
419+
resumedState = pickedState;
416420
if (pickedState !== null) {
417421
startedAt = pickedState.startedAt;
418422
runTaskTitle = pickedState.task;
@@ -435,11 +439,11 @@ export async function runTUI(initialConfig: Config): Promise<number> {
435439
// run.json at all.
436440
await saveState(config.cwd, sessionId, {
437441
status: "running",
438-
turnsUsed: 0,
442+
turnsUsed: resumedState?.turnsUsed ?? 0,
439443
task: runTaskTitle.trim().length > 0 ? runTaskTitle.trim() : "(conversation)",
440444
startedAt,
441445
model: `${config.providerName}:${config.model}`,
442-
mcpServers: [],
446+
mcpServers: resumedState?.mcpServers ?? [],
443447
});
444448

445449
// Crash guard: if anything from setup onward throws all the way out of
@@ -1305,6 +1309,7 @@ export async function runTUI(initialConfig: Config): Promise<number> {
13051309
const runSink = createRunSink({
13061310
emitter,
13071311
hookManager,
1312+
...(resumedState !== null ? { initialTurnCount: resumedState.turnsUsed } : {}),
13081313
onTurnComplete: (ctx) => {
13091314
// provider_id is the canonical provider kind, never ctx.source.sourceId:
13101315
// sourceId is the user-typed label from onboarding/settings, and free
@@ -1324,7 +1329,7 @@ export async function runTUI(initialConfig: Config): Promise<number> {
13241329

13251330
// MCP servers connected so far, keyed by name so a reconnect after a failure
13261331
// replaces rather than duplicates the entry.
1327-
let connectedMcpServers: ConnectedMcpServer[] = [];
1332+
let connectedMcpServers: ConnectedMcpServer[] = resumedState?.mcpServers ?? [];
13281333
// Every configured server's latest state, for the /mcp surface. Unlike
13291334
// `connectedMcpServers` (persisted run metadata) this keeps the ones that
13301335
// failed or are still waiting on authorization.

0 commit comments

Comments
 (0)