fix: make compaction activation atomic
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user