From ce48080a2207423b9f0c71f0739a8f462b602e25 Mon Sep 17 00:00:00 2001 From: spoukyii <61633921+spoukyii@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:07:25 -0800 Subject: [PATCH 1/2] test(server): usage-limit recovery skips subagents of archived and deleted parents --- .../orchestration-v2/ProjectionStore.test.ts | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) 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. From fa3f5c029e51a6c31f269ed8b272ee4002cf9068 Mon Sep 17 00:00:00 2001 From: spoukyii <61633921+spoukyii@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:08:33 -0800 Subject: [PATCH 2/2] fix(server): usage-limit auto-resume no longer arms subagents of archived or deleted parents #27951 stopped the usage-limit recovery sweep from freshly arming a subagent whose parent is settled. The same query still armed a subagent whose parent was archived or deleted, so turning on Resume after usage limits (or snooze) resumed abandoned children at their reset and spent the user's quota on a turn whose completion cannot wake the parent: delivery to an archived or deleted parent is disposed. The parent check in getLimitRecoveryCandidates now treats an archived or deleted parent the same as a settled one. The resume branch is unchanged, as in #27951. Bug: usage-limit auto-resume arms subagents of archived or deleted parents --- apps/server/src/orchestration-v2/ProjectionStore.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 + ) ) ) )