fix: finalize stopped subworker sessions
This commit is contained in:
@@ -178,4 +178,4 @@ in_flight?: InFlightSnapshot,
|
||||
* Parent-owned Internal Worker sessions visible to this client.
|
||||
* Service-private Internal Workers are deliberately excluded.
|
||||
*/
|
||||
internal_workers?: Array<InternalWorkerSnapshot>, } } | { "event": "internal_worker", "data": { worker: InternalWorkerRef, revision: number, event: Event, } } | { "event": "segment_rotated", "data": { entry: unknown, } } | { "event": "status", "data": { status: WorkerStatus, } } | { "event": "completions", "data": { kind: CompletionKind, entries: Array<CompletionEntry>, } } | { "event": "rewind_targets", "data": { head_entries: number, targets: Array<RewindTarget>, } } | { "event": "rewind_applied", "data": { entries: Array<unknown>, input: Array<Segment>, summary: RewindSummary, } } | { "event": "workers_listed", "data": { workers: unknown, } } | { "event": "worker_restored", "data": { result: unknown, } } | { "event": "peer_registered", "data": { result: unknown, } } | { "event": "alert", "data": Alert } | { "event": "memory_worker", "data": MemoryWorkerEvent } | { "event": "compact_start" } | { "event": "compact_done", "data": { new_segment_id: string, } } | { "event": "compact_failed", "data": { error: string, } } | { "event": "shutdown" };
|
||||
internal_workers?: Array<InternalWorkerSnapshot>, } } | { "event": "internal_worker", "data": { worker: InternalWorkerRef, revision: number, event: Event, } } | { "event": "internal_worker_removed", "data": { worker: InternalWorkerRef, revision: number, } } | { "event": "segment_rotated", "data": { entry: unknown, } } | { "event": "status", "data": { status: WorkerStatus, } } | { "event": "completions", "data": { kind: CompletionKind, entries: Array<CompletionEntry>, } } | { "event": "rewind_targets", "data": { head_entries: number, targets: Array<RewindTarget>, } } | { "event": "rewind_applied", "data": { entries: Array<unknown>, input: Array<Segment>, summary: RewindSummary, } } | { "event": "workers_listed", "data": { workers: unknown, } } | { "event": "worker_restored", "data": { result: unknown, } } | { "event": "peer_registered", "data": { result: unknown, } } | { "event": "alert", "data": Alert } | { "event": "memory_worker", "data": MemoryWorkerEvent } | { "event": "compact_start" } | { "event": "compact_done", "data": { new_segment_id: string, } } | { "event": "compact_failed", "data": { error: string, } } | { "event": "shutdown" };
|
||||
|
||||
@@ -1287,7 +1287,10 @@ Deno.test("Internal Worker output stays separate and revision-fenced", () => {
|
||||
}]);
|
||||
assertEquals(projection.lines, []);
|
||||
assertEquals(projection.internalWorkers.length, 1);
|
||||
assertEquals(projection.internalWorkers[0].console.lines[0].body, "child output");
|
||||
assertEquals(
|
||||
projection.internalWorkers[0].console.lines[0].body,
|
||||
"child output",
|
||||
);
|
||||
|
||||
projection = projector.append([{
|
||||
eventId: "2",
|
||||
@@ -1364,15 +1367,112 @@ Deno.test("parent snapshot authoritatively replaces Internal Worker projections"
|
||||
},
|
||||
}]);
|
||||
const projection = projector.append([{ eventId: "snapshot", event }]);
|
||||
assertEquals(projection.internalWorkers.map((worker) => worker.worker.session_id), [
|
||||
"replacement",
|
||||
]);
|
||||
assertEquals(
|
||||
projection.internalWorkers.map((worker) => worker.worker.session_id),
|
||||
[
|
||||
"replacement",
|
||||
],
|
||||
);
|
||||
const childLines = projection.internalWorkers[0].console.lines;
|
||||
assertEquals(childLines.length, 1);
|
||||
assertEquals(new Set(childLines.map((line) => line.id)).size, 1);
|
||||
assertEquals(childLines[0].kind, "tool");
|
||||
});
|
||||
|
||||
Deno.test("terminal Internal Worker removal drops descendants and fences late events", () => {
|
||||
const worker = {
|
||||
session_id: "child-session",
|
||||
name: "child",
|
||||
parent_session_id: "parent-session",
|
||||
kind: "sub_worker" as const,
|
||||
};
|
||||
const nestedWorker = {
|
||||
session_id: "grandchild-session",
|
||||
name: "grandchild",
|
||||
parent_session_id: "child-session",
|
||||
kind: "sub_worker" as const,
|
||||
};
|
||||
const projector = createConsoleProjector();
|
||||
let projection = projector.append([{
|
||||
eventId: "child",
|
||||
event: {
|
||||
event: "internal_worker",
|
||||
data: {
|
||||
worker,
|
||||
revision: 2,
|
||||
event: {
|
||||
event: "internal_worker",
|
||||
data: {
|
||||
worker: nestedWorker,
|
||||
revision: 1,
|
||||
event: { event: "text_done", data: { text: "nested" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}]);
|
||||
assertEquals(projection.internalWorkers.length, 1);
|
||||
assertEquals(
|
||||
projection.internalWorkers[0].console.internalWorkers.length,
|
||||
1,
|
||||
);
|
||||
|
||||
projection = projector.append([{
|
||||
eventId: "removed",
|
||||
event: {
|
||||
event: "internal_worker_removed",
|
||||
data: { worker, revision: 3 },
|
||||
},
|
||||
}, {
|
||||
eventId: "late",
|
||||
event: {
|
||||
event: "internal_worker",
|
||||
data: {
|
||||
worker,
|
||||
revision: 4,
|
||||
event: { event: "text_done", data: { text: "must stay removed" } },
|
||||
},
|
||||
},
|
||||
}]);
|
||||
assertEquals(projection.internalWorkers, []);
|
||||
|
||||
const snapshot = snapshotEvent("/repo");
|
||||
projection = projector.append([{ eventId: "snapshot", event: snapshot }]);
|
||||
assertEquals(projection.internalWorkers, []);
|
||||
assertEquals(projection.removedInternalWorkers, {});
|
||||
});
|
||||
|
||||
Deno.test("stale Internal Worker removal cannot discard a newer projection", () => {
|
||||
const worker = {
|
||||
session_id: "child-session",
|
||||
name: "child",
|
||||
parent_session_id: "parent-session",
|
||||
kind: "sub_worker" as const,
|
||||
};
|
||||
const projector = createConsoleProjector();
|
||||
projector.append([{
|
||||
eventId: "current",
|
||||
event: {
|
||||
event: "internal_worker",
|
||||
data: {
|
||||
worker,
|
||||
revision: 4,
|
||||
event: { event: "text_done", data: { text: "current" } },
|
||||
},
|
||||
},
|
||||
}]);
|
||||
|
||||
const projection = projector.append([{
|
||||
eventId: "stale-removal",
|
||||
event: {
|
||||
event: "internal_worker_removed",
|
||||
data: { worker, revision: 3 },
|
||||
},
|
||||
}]);
|
||||
assertEquals(projection.internalWorkers.length, 1);
|
||||
assertEquals(projection.internalWorkers[0].revision, 4);
|
||||
});
|
||||
|
||||
Deno.test("snapshot restores TaskStore state from system history", () => {
|
||||
const taskSnapshot =
|
||||
`[Session TaskStore snapshot]\n\n\`\`\`json\n{\n "tasks": [{"taskid": 3, "status": "pending", "subject": "Restored", "description": "From compaction"}]\n}\n\`\`\``;
|
||||
|
||||
@@ -94,6 +94,8 @@ export type ConsoleProjection = {
|
||||
cwd: string | null;
|
||||
lastEventId: string | null;
|
||||
internalWorkers: InternalWorkerProjection[];
|
||||
/** Terminal child-session fences, reset only by an authoritative snapshot. */
|
||||
removedInternalWorkers: Record<string, number>;
|
||||
};
|
||||
|
||||
export type ConsoleTimelineLineSelection = {
|
||||
@@ -179,6 +181,7 @@ export function emptyConsoleProjection(): ConsoleProjection {
|
||||
cwd: null,
|
||||
lastEventId: null,
|
||||
internalWorkers: [],
|
||||
removedInternalWorkers: {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -286,6 +289,7 @@ export function applyProtocolEvent(
|
||||
cwd: projection.cwd,
|
||||
lastEventId: envelope.eventId,
|
||||
internalWorkers: [...projection.internalWorkers],
|
||||
removedInternalWorkers: { ...projection.removedInternalWorkers },
|
||||
};
|
||||
const event = envelope.event;
|
||||
|
||||
@@ -406,9 +410,16 @@ export function applyProtocolEvent(
|
||||
next.internalWorkers = (event.data.internal_workers ?? []).map((worker) =>
|
||||
projectInternalWorkerSnapshot(worker, envelope.eventId, next.cwd)
|
||||
);
|
||||
next.removedInternalWorkers = {};
|
||||
break;
|
||||
}
|
||||
case "internal_worker": {
|
||||
if (
|
||||
Object.hasOwn(
|
||||
next.removedInternalWorkers,
|
||||
event.data.worker.session_id,
|
||||
)
|
||||
) break;
|
||||
const existingIndex = next.internalWorkers.findIndex((worker) =>
|
||||
worker.worker.session_id === event.data.worker.session_id
|
||||
);
|
||||
@@ -433,6 +444,19 @@ export function applyProtocolEvent(
|
||||
else next.internalWorkers.push(updated);
|
||||
break;
|
||||
}
|
||||
case "internal_worker_removed": {
|
||||
const existingIndex = next.internalWorkers.findIndex((worker) =>
|
||||
worker.worker.session_id === event.data.worker.session_id
|
||||
);
|
||||
const existingRevision = existingIndex >= 0
|
||||
? next.internalWorkers[existingIndex].revision
|
||||
: 0;
|
||||
if (event.data.revision <= existingRevision) break;
|
||||
next.removedInternalWorkers[event.data.worker.session_id] =
|
||||
event.data.revision;
|
||||
if (existingIndex >= 0) next.internalWorkers.splice(existingIndex, 1);
|
||||
break;
|
||||
}
|
||||
case "status":
|
||||
next.status = event.data.status;
|
||||
break;
|
||||
@@ -1294,6 +1318,7 @@ function snapshotProjectionFromEntries(
|
||||
cwd,
|
||||
lastEventId: eventId,
|
||||
internalWorkers: [],
|
||||
removedInternalWorkers: {},
|
||||
};
|
||||
entries.forEach((entry, index) =>
|
||||
applyLogEntry(projection, `${eventId}-snapshot-${index}`, entry)
|
||||
|
||||
Reference in New Issue
Block a user