From f80c651e131aef0a9052d7ef37228acb400c8a9e Mon Sep 17 00:00:00 2001 From: Byntham <135178921+Byntham@users.noreply.github.com> Date: Fri, 30 Oct 2026 01:57:10 -0700 Subject: [PATCH] fix(server): delegated tasks no longer wait on background work the child left running Co-Authored-By: Claude Opus 5.5 --- .../DelegatedCompletionDelivery.test.ts | 9 ++++-- .../src/orchestration-v2/Orchestrator.ts | 5 ++-- .../SubagentProjection.test.ts | 28 +++++++++++++------ .../src/server/subagentProjection.ts | 5 +++- 4 files changed, 32 insertions(+), 15 deletions(-) diff --git a/apps/server/src/orchestration-v2/DelegatedCompletionDelivery.test.ts b/apps/server/src/orchestration-v2/DelegatedCompletionDelivery.test.ts index ad2bc395d083f..cb12f98347533 100644 --- a/apps/server/src/orchestration-v2/DelegatedCompletionDelivery.test.ts +++ b/apps/server/src/orchestration-v2/DelegatedCompletionDelivery.test.ts @@ -3134,9 +3134,12 @@ it.layer(layerTest)("delegated tasks across a server restart", (it) => { occurredAt: now, payload: { ...providerThread, - pendingBackgroundTasks: open - ? [{ taskId: "sleep-900", kind: "command" as const }] - : [], + pendingBackgroundTasks: [ + ...(open + ? [{ taskId: "sleep-900", kind: "command" as const, awaited: true as const }] + : []), + { taskId: "log-watcher", kind: "monitor" as const }, + ], }, } : { diff --git a/apps/server/src/orchestration-v2/Orchestrator.ts b/apps/server/src/orchestration-v2/Orchestrator.ts index 50f9a2acc0f39..f7aa4d5e1ca37 100644 --- a/apps/server/src/orchestration-v2/Orchestrator.ts +++ b/apps/server/src/orchestration-v2/Orchestrator.ts @@ -74,6 +74,7 @@ import { import { modelSelectionsEqual } from "@t3tools/shared/model"; import { recordThreadOpen } from "@t3tools/shared/paletteSearch"; import { + backgroundWorkHoldsCompletion, derivePendingBackgroundWork, pendingBackgroundTurnItems, } from "@t3tools/shared/orchestrationV2PendingBackgroundWork"; @@ -13342,14 +13343,14 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio // A child whose run ends while its background tasks or its own subagents still run has no // result yet. When that work settles there is no run update to finalize it, so these events - // do it. A background roster counts once it empties after it was seen non-empty. + // do it. A background roster counts once it stops holding after it was seen holding. const providerThreadsWithBackgroundWork = new Set(); const settlesChildWork = (stored: OrchestrationV2StoredEvent) => { const { event } = stored; if (event.type === "subagent.updated") return !isOrchestrationV2WorkActive(event.payload.status); if (event.type !== "provider-thread.updated") return false; - if ((event.payload.pendingBackgroundTasks?.length ?? 0) > 0) { + if (backgroundWorkHoldsCompletion(event.payload.pendingBackgroundTasks ?? [])) { providerThreadsWithBackgroundWork.add(event.payload.id); return false; } diff --git a/apps/server/src/orchestration-v2/SubagentProjection.test.ts b/apps/server/src/orchestration-v2/SubagentProjection.test.ts index 95abcbaf5c1a8..db2e89dced298 100644 --- a/apps/server/src/orchestration-v2/SubagentProjection.test.ts +++ b/apps/server/src/orchestration-v2/SubagentProjection.test.ts @@ -262,15 +262,25 @@ it("waits for nested work and retains the report across monitor acknowledgements "result_available", ); } - assert.equal( - delegatedTaskProgress({ - ...projection, - providerThreads: [ - { pendingBackgroundTasks: [{ taskId: "background-audit", kind: "command" }] }, - ], - }).state, - "waiting_for_children", - ); + for (const [awaited, state] of [ + [true, "waiting_for_children"], + [false, "result_available"], + ] as const) { + assert.equal( + delegatedTaskProgress({ + ...projection, + providerThreads: [ + { + pendingBackgroundTasks: [ + { taskId: "background-audit", kind: "command", ...(awaited ? { awaited } : {}) }, + { taskId: "log-watcher", kind: "monitor" }, + ], + }, + ], + }).state, + state, + ); + } const pending = { ...run, id: RunId.make("followup"), diff --git a/packages/provider-core/src/server/subagentProjection.ts b/packages/provider-core/src/server/subagentProjection.ts index be8c4bf98aa54..866d43fb2db08 100644 --- a/packages/provider-core/src/server/subagentProjection.ts +++ b/packages/provider-core/src/server/subagentProjection.ts @@ -18,6 +18,7 @@ import type { ThreadId, TurnItemId, } from "@t3tools/contracts"; +import { backgroundWorkHoldsCompletion } from "@t3tools/shared/orchestrationV2PendingBackgroundWork"; import { runRanAfter } from "@t3tools/shared/orchestrationV2ThreadError"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; @@ -256,7 +257,9 @@ export function delegatedTaskProgress(projection: { (task.completionDelivery?.state === "pending" || task.completionDelivery?.state === "claimed")), ) || - projection.providerThreads.some((thread) => (thread.pendingBackgroundTasks?.length ?? 0) > 0); + projection.providerThreads.some((thread) => + backgroundWorkHoldsCompletion(thread.pendingBackgroundTasks ?? []), + ); const resultRun = workRuns .filter((run) => terminal(run.status) && (run.startedAt !== null || run.ordinal === 1)) .toSorted((a, b) => (runRanAfter(a, b) ? -1 : runRanAfter(b, a) ? 1 : 0))[0];