fix: make compaction activation atomic

This commit is contained in:
2026-09-16 03:44:48 +09:00
parent 016dbd7cb1
commit 4e7a314a00
9 changed files with 523 additions and 67 deletions
+10 -1
View File
@@ -113,7 +113,16 @@ export type RewindSummary = { truncated_to_entries: number, discarded_entries: n
export type InFlightBlock = { "kind": "text", text: string, finished?: boolean, } | { "kind": "thinking", text: string, finished?: boolean, } | { "kind": "tool_call", id: string, name: string, args: string, state?: InFlightToolCallState, };
export type InFlightSnapshot = { blocks?: Array<InFlightBlock>, commands?: Array<CommandSnapshot>, };
export type InFlightCompaction = { schema_version: number, compaction_id: string, revision: number, internal_worker: InternalWorkerRef | null, started_at_ms: number, };
export type InFlightSnapshot = { blocks?: Array<InFlightBlock>, commands?: Array<CommandSnapshot>,
/**
* The currently running compaction, if any.
*
* This is lifecycle progress only. Candidate history and the staged
* Segment remain private until the Segment is activated atomically.
*/
compaction?: InFlightCompaction | null, };
export type SessionEntryProvenance = "human_input" | "worker_input" | "flow_instruction" | "backend_instruction" | "model_output" | "tool_output" | "derived_summary" | "legacy_unknown";
@@ -1079,6 +1079,27 @@ Deno.test("projectConsole upserts compaction lifecycle by stable id", () => {
assertEquals(completed.lines[0].streaming, false);
});
Deno.test("snapshot restores running compaction without staged content", () => {
const snapshot = snapshotEvent("/repo") as Extract<Event, { event: "snapshot" }>;
snapshot.data.in_flight = {
blocks: [],
compaction: {
schema_version: 3,
compaction_id: "compaction-snapshot",
revision: 1,
internal_worker: null,
started_at_ms: 1_000,
},
};
const projection = projectConsole([{ eventId: "snapshot", event: snapshot }]);
assertEquals(projection.lines.length, 1);
assertEquals(projection.lines[0].id, "compaction-compaction-snapshot");
assertEquals(projection.lines[0].compaction?.state, "running");
assertEquals(projection.lines[0].body.includes("staged"), false);
});
Deno.test("compaction service activity stays nested in one lifecycle item", () => {
const worker = {
session_id: "compactor-session",
@@ -6,6 +6,7 @@ import type {
CompactionLifecycle,
Event as ProtocolEvent,
InFlightBlock,
InFlightCompaction,
InFlightToolCallState,
InternalWorkerRef,
InternalWorkerSnapshot,
@@ -638,7 +639,7 @@ function projectInternalWorkerSnapshot(
eventId: string,
cwd: string | null,
): InternalWorkerProjection {
const console = snapshotProjectionFromSession(
let console = snapshotProjectionFromSession(
`${eventId}:internal:${snapshot.worker.session_id}:snapshot`,
snapshot.session,
cwd,
@@ -669,6 +670,9 @@ function projectInternalWorkerSnapshot(
console.internalWorkers = (snapshot.internal_workers ?? []).map((child) =>
projectInternalWorkerSnapshot(child, eventId, cwd)
);
if (snapshot.in_flight?.compaction) {
console = applyInFlightCompaction(console, snapshot.in_flight.compaction);
}
return { worker: snapshot.worker, revision: snapshot.revision, console };
}
@@ -714,6 +718,20 @@ function compactionActivity(
.filter((value, index, values) => value.length > 0 && values.indexOf(value) === index);
}
function applyInFlightCompaction(
projection: ConsoleProjection,
progress: InFlightCompaction,
): ConsoleProjection {
return applyCompactionLifecycle(projection, {
...progress,
state: "running",
ended_at_ms: null,
summary: null,
error: null,
new_segment_id: null,
});
}
function applyCompactionLifecycle(
projection: ConsoleProjection,
lifecycle: CompactionLifecycle
@@ -970,6 +988,13 @@ export function applyProtocolEvent(
projectInternalWorkerSnapshot(worker, envelope.eventId, next.cwd)
);
next.removedInternalWorkers = {};
if (event.data.in_flight?.compaction) {
const withCompaction = applyInFlightCompaction(
next,
event.data.in_flight.compaction,
);
next.lines = withCompaction.lines;
}
for (const line of next.lines) {
const compaction = line.compaction;
if (!compaction) continue;
@@ -981,19 +1006,6 @@ export function applyProtocolEvent(
activity: compactionActivity(next, sessionId),
};
}
if (
compaction.state === "running" &&
(!sessionId || !next.internalWorkers.some((worker) =>
worker.worker.session_id === sessionId
))
) {
line.streaming = false;
line.compaction = {
...line.compaction!,
state: "interrupted",
endedAtMs: envelope.observedAtMs ?? Date.now(),
};
}
}
applyWorkerStateSnapshot(next, event.data.state, envelope.eventId);
break;