diff --git a/apps/server/src/orchestration-v2/ProjectionStore.test.ts b/apps/server/src/orchestration-v2/ProjectionStore.test.ts index a79b2f37a1ddf..978677491ea08 100644 --- a/apps/server/src/orchestration-v2/ProjectionStore.test.ts +++ b/apps/server/src/orchestration-v2/ProjectionStore.test.ts @@ -5550,15 +5550,16 @@ it.layer(layerTest)("ProjectionStoreV2", (it) => { }), ); - it.effect("does not freshly arm limit recovery for a subagent of a settled parent", () => + it.effect("does not freshly arm limit recovery for a subagent of a put-away parent", () => Effect.gen(function* () { const store = yield* ProjectionStore.ProjectionStoreV2; const now = yield* DateTime.now; const resetAt = DateTime.formatIso(DateTime.add(now, { hours: 1 })); + type ParentState = "settled" | "archived" | "deleted" | null; const threadPayload = ( threadId: ThreadId, parentThreadId: ThreadId | null, - settledOverride: "settled" | null, + state: ParentState, ) => ({ createdBy: "user", @@ -5581,30 +5582,30 @@ it.layer(layerTest)("ProjectionStoreV2", (it) => { forkedFrom: null, createdAt: now, updatedAt: now, - archivedAt: null, - settledOverride, - settledAt: settledOverride === null ? null : now, + archivedAt: state === "archived" ? now : null, + settledOverride: state === "settled" ? ("settled" as const) : null, + settledAt: state === "settled" ? now : null, lastVisitedAt: null, - deletedAt: null, + deletedAt: state === "deleted" ? now : null, }) as const; const createThread = ( threadId: ThreadId, parentThreadId: ThreadId | null, - settledOverride: "settled" | null, + state: ParentState, ) => store.apply({ id: EventId.make(`event:${threadId}:created`), type: "thread.created", threadId, occurredAt: now, - payload: threadPayload(threadId, parentThreadId, settledOverride), + payload: threadPayload(threadId, parentThreadId, state), }); - const addLimitedChild = Effect.fnUntraced(function* (name: string, parentSettled: boolean) { + const addLimitedChild = Effect.fnUntraced(function* (name: string, parentState: ParentState) { const parentId = ThreadId.make(`thread:settled-parent-limit:${name}:parent`); const childId = ThreadId.make(`thread:settled-parent-limit:${name}:child`); const runId = RunId.make(`run:settled-parent-limit:${name}`); const rootNodeId = NodeId.make(`node:settled-parent-limit:${name}`); - yield* createThread(parentId, null, parentSettled ? "settled" : null); + yield* createThread(parentId, null, parentState); yield* createThread(childId, parentId, null); const run = { id: runId, @@ -5666,14 +5667,22 @@ it.layer(layerTest)("ProjectionStoreV2", (it) => { }); return childId; }); - const settledParentChild = yield* addLimitedChild("settled", true); - const activeParentChild = yield* addLimitedChild("active", false); + const settledParentChild = yield* addLimitedChild("settled", "settled"); + const archivedParentChild = yield* addLimitedChild("archived", "archived"); + const deletedParentChild = yield* addLimitedChild("deleted", "deleted"); + const activeParentChild = yield* addLimitedChild("active", null); + const children = [ + settledParentChild, + archivedParentChild, + deletedParentChild, + activeParentChild, + ]; for (const options of [ { now, autoResume: true, snooze: false }, { now, autoResume: false, snooze: true }, ]) { const armed = (yield* store.getLimitRecoveryCandidates(options)) - .filter((row) => row.id === settledParentChild || row.id === activeParentChild) + .filter((row) => children.includes(row.id)) .filter( (row) => limitRecoveryCommand( @@ -5687,7 +5696,7 @@ it.layer(layerTest)("ProjectionStoreV2", (it) => { assert.deepEqual( armed, [activeParentChild], - `only the child of the unsettled parent is armed (${JSON.stringify(options)})`, + `only the child of the open parent is armed (${JSON.stringify(options)})`, ); } // A recovery armed before the parent settled still resumes at its reset. diff --git a/apps/server/src/orchestration-v2/ProjectionStore.ts b/apps/server/src/orchestration-v2/ProjectionStore.ts index 6186db8c6e39d..c4e40a7a1268b 100644 --- a/apps/server/src/orchestration-v2/ProjectionStore.ts +++ b/apps/server/src/orchestration-v2/ProjectionStore.ts @@ -3979,12 +3979,16 @@ export const layer: Layer.Layer = ${booleanInt(options.autoResume)} OR (${booleanInt(options.snooze)} AND julianday(json_extract(item.payload_json, '$.failure.resetAt')) > julianday(${DateTime.formatIso(options.now)})) ) - -- A subagent of a settled parent is abandoned work; resuming it would wake the parent. + -- A subagent of a settled, archived or deleted parent is abandoned work. AND NOT EXISTS ( SELECT 1 FROM orchestration_v2_projection_threads parent WHERE json_extract(t.payload_json, '$.lineage.relationshipToParent') = 'subagent' AND parent.thread_id = json_extract(t.payload_json, '$.lineage.parentThreadId') - AND json_extract(parent.payload_json, '$.settledOverride') = 'settled' + AND ( + json_extract(parent.payload_json, '$.settledOverride') = 'settled' + OR json_extract(parent.payload_json, '$.archivedAt') IS NOT NULL + OR parent.deleted_at IS NOT NULL + ) ) ) )