Skip to content

Commit fbd385c

Browse files
committed
refactor(session): drop the run.json snapshot debounce
1 parent fe841a9 commit fbd385c

6 files changed

Lines changed: 6 additions & 188 deletions

File tree

‎src/exec/runner.ts‎

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ import {
102102
clearActiveDisposeHost,
103103
} from "../session/active-host.js";
104104
import {
105-
createRunSnapshotDebouncer,
106105
finalizeRunState,
107106
loadState,
108107
saveState,
@@ -536,18 +535,13 @@ export async function runExec(config: Config): Promise<ExecResult> {
536535
: {}),
537536
};
538537
let stopHeartbeat: (() => void) | undefined;
539-
// CL-10667: debounced per-turn boundary snapshots; terminal exposes flush.
540-
let runSnapshotDebouncer:
541-
| ReturnType<typeof createRunSnapshotDebouncer>
542-
| undefined;
543538

544539
const persist = async (
545540
status: "running" | "done" | "failed" | "cancelled",
546541
extra?: { error?: string },
547542
): Promise<void> => {
548543
if (finalized && status === "running") return;
549544
if (status !== "running") {
550-
await runSnapshotDebouncer?.flush();
551545
finalized = true;
552546
stopHeartbeat?.();
553547
stopHeartbeat = undefined;
@@ -601,9 +595,6 @@ export async function runExec(config: Config): Promise<ExecResult> {
601595
};
602596

603597
await persist("running");
604-
runSnapshotDebouncer = createRunSnapshotDebouncer({
605-
write: () => persist("running"),
606-
});
607598
stopHeartbeat = startRunHeartbeat({
608599
shouldTick: () => !finalized,
609600
tick: () => persist("running"),
@@ -1057,8 +1048,7 @@ export async function runExec(config: Config): Promise<ExecResult> {
10571048
activeRunHandle.cacheWriteModel = `${event.data.source.provider}:${event.data.source.model}`;
10581049
}
10591050
}
1060-
// CL-10667: debounced — the write recomputes state when it fires.
1061-
runSnapshotDebouncer?.schedule();
1051+
void persist("running");
10621052
},
10631053
resolveContextDir: () => workdir,
10641054
});

‎src/session/run-snapshot-debounce.test.ts‎

Lines changed: 0 additions & 82 deletions
This file was deleted.

‎src/session/state.ts‎

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -131,62 +131,6 @@ export async function saveState(
131131
return write;
132132
}
133133

134-
/**
135-
* Debounce window for mid-run `running` snapshots (CL-10667): coalesces the
136-
* per-turn boundary write to ~0.2/turn at a ~1s/turn cadence, while the
137-
* run-liveness heartbeat keeps mtime freshness over longer gaps.
138-
*/
139-
export const RUN_SNAPSHOT_DEBOUNCE_MS = 5_000;
140-
141-
export interface RunSnapshotDebouncer {
142-
/** Mark a pending write and arm the one-shot timer once. */
143-
schedule: () => void;
144-
/** Cancel the timer and run any pending write immediately. */
145-
flush: () => Promise<void>;
146-
}
147-
148-
/**
149-
* Lazy payoff: the deferred write recomputes its snapshot when it fires, not
150-
* at schedule time, so a front of rapid turns persists the state current at
151-
* fire time. One timer; `schedule` while armed only re-marks dirty. The
152-
* writer maps write failures to undefined so a stalled flush cannot wedge
153-
* the terminal path that awaits it.
154-
*/
155-
export function createRunSnapshotDebouncer(args: {
156-
write: () => Promise<void>;
157-
intervalMs?: number;
158-
}): RunSnapshotDebouncer {
159-
const intervalMs = args.intervalMs ?? RUN_SNAPSHOT_DEBOUNCE_MS;
160-
let timer: ReturnType<typeof setTimeout> | null = null;
161-
let dirty = false;
162-
let inFlight: Promise<void> = Promise.resolve();
163-
164-
const run = (): void => {
165-
dirty = false;
166-
inFlight = args.write().catch(() => undefined);
167-
};
168-
169-
return {
170-
schedule: () => {
171-
dirty = true;
172-
if (timer !== null) return;
173-
timer = setTimeout(() => {
174-
timer = null;
175-
if (dirty) run();
176-
}, intervalMs);
177-
timer.unref?.();
178-
},
179-
flush: async () => {
180-
if (timer !== null) {
181-
clearTimeout(timer);
182-
timer = null;
183-
}
184-
if (dirty) run();
185-
await inFlight;
186-
},
187-
};
188-
}
189-
190134
// Terminal-write path: clears the in-memory active-run handle (active-run.ts)
191135
// alongside the on-disk status so the two cannot drift. Non-terminal
192136
// ("running") snapshots use saveState directly.

‎src/tui/runner/exit.ts‎

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import { setAgentSourceUnlessClosed } from "../agent-source-sync.js";
2424
import { billingIdentityFromSource } from "../../cost/session-cost.js";
2525
import { createRunSummary, type RunSummary } from "../../session/hooks.js";
2626
import {
27-
createRunSnapshotDebouncer,
2827
finalizeRunState,
2928
saveState,
3029
type RunState,
@@ -262,20 +261,10 @@ export function createRunPersistence(
262261
await writeRunSnapshot("running");
263262
};
264263

265-
// CL-10667: per-turn boundary snapshots coalesce behind the debounce
266-
// window (lazy payoff — the deferred write recomputes state at fire time,
267-
// so nothing scheduled goes stale).
268-
// Observable exposes flush before they observe or supersede run.json.
269-
const runSnapshotDebouncer = createRunSnapshotDebouncer({
270-
write: () => persistRunSnapshot("running"),
271-
});
272-
273264
return {
274265
writeRunSnapshot,
275266
persistRunSnapshot,
276267
heartbeatRunSnapshot,
277-
scheduleRunSnapshot: runSnapshotDebouncer.schedule,
278-
flushRunSnapshot: runSnapshotDebouncer.flush,
279268
};
280269
}
281270

@@ -322,18 +311,11 @@ export async function createRunLifecycle(
322311
newSession: () => void;
323312
agentProxy: Agent;
324313
}> {
325-
const {
326-
persistRunSnapshot,
327-
heartbeatRunSnapshot,
328-
scheduleRunSnapshot,
329-
flushRunSnapshot,
330-
} = createRunPersistence(state, services);
314+
const { persistRunSnapshot, heartbeatRunSnapshot } = createRunPersistence(
315+
state,
316+
services,
317+
);
331318
state.persistRunSnapshot = persistRunSnapshot;
332-
// CL-10667: per-turn boundary snapshots schedule behind the debounce
333-
// window; observable exposes (interrupt rebuild, rotation, finalize) flush
334-
// the pending write before they observe or supersede run.json.
335-
state.scheduleRunSnapshot = scheduleRunSnapshot;
336-
state.flushRunSnapshot = flushRunSnapshot;
337319
const activateAndCommitWire = (names: string[]): void => {
338320
if (!services.activatedToolNames.activate(names)) return;
339321
if (services.flushPromotions()) {
@@ -618,10 +600,6 @@ export async function createRunLifecycle(
618600
enqueue: enqueueOp,
619601
rebuild: async () => {
620602
try {
621-
// CL-10667: run.json is observable the moment the rebuild starts
622-
// reading store state, so the pending debounced snapshot flushes
623-
// first.
624-
await state.flushRunSnapshot?.();
625603
// close() tears down stream consumers before the aborted cycle's
626604
// inference.error is delivered, so the recorder never sees a
627605
// terminal event. Dispose before that teardown salvages the buffer
@@ -702,10 +680,6 @@ export async function createRunLifecycle(
702680
"stream drain during session-rotation teardown failed",
703681
),
704682
);
705-
// CL-10667: the rotation terminal write supersets the pending
706-
// snapshot for the outgoing session; flush so the debounced write
707-
// cannot straddle the id repoint below.
708-
await state.flushRunSnapshot?.();
709683
await persistRunSnapshot(
710684
"done",
711685
{ finishedAt: Date.now() },
@@ -789,9 +763,6 @@ export async function finalizeTUIRun(
789763
await hostOf(state).waitUntilExit();
790764
state.stopRunHeartbeat?.();
791765
delete state.stopRunHeartbeat;
792-
// CL-10667: pending debounced snapshots flush ahead of the terminal write
793-
// so a failed or dropped terminal write cannot leave run.json stale.
794-
await state.flushRunSnapshot?.();
795766
// Stop workers before awaiting the session-op tail so a hung enqueue cannot
796767
// delay abort/reap; persistence, hooks, and telemetry stay after stop.
797768
// Toolset dispose lives inside shutdownRuntime so quit, crash, and signals

‎src/tui/runner/session.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,7 @@ export async function assembleTUISession(
176176
start.activeRunHandle.cacheWriteModel = `${event.data.source.provider}:${event.data.source.model}`;
177177
}
178178
}
179-
// CL-10667: debounced — the write recomputes state when it fires.
180-
state.scheduleRunSnapshot?.();
179+
void state.persistRunSnapshot?.("running");
181180
},
182181
hookEnabled: initialHookEnabled,
183182
onHookEvent: (event) => emitter.emit("hook", event),

‎src/tui/runner/state.ts‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -332,10 +332,6 @@ export interface RunnerState {
332332
extra?: SnapshotExtra,
333333
kind?: Exclude<SnapshotKind, "run-end">,
334334
) => Promise<void>;
335-
/** CL-10667: debounced per-turn boundary snapshot writer. */
336-
scheduleRunSnapshot?: () => void;
337-
/** CL-10667: flush any pending debounced snapshot before observing run.json. */
338-
flushRunSnapshot?: () => Promise<void>;
339335
shutdownRuntime?: () => Promise<void>;
340336
stopFleetReporting?: () => void;
341337
stopRunHeartbeat?: () => void;

0 commit comments

Comments
 (0)