From 6080a602a59fbd49fc1311c91778e622f9f69a5c Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 25 Oct 2026 21:51:53 +0000 Subject: [PATCH 1/6] test(server): foreground subagent background tasks cannot be awaited Red test for #24956. Co-Authored-By: Claude Opus 5.5 --- .../Adapters/ClaudeAdapterV2.test.ts | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 5dfbffa595908..c886953d4b068 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -7524,6 +7524,103 @@ describe("ClaudeAdapterV2 background wake turns", () => { ), ); + it.effect("does not mark tasks owned by a foreground subagent as awaited", () => + Effect.scoped( + Effect.gen(function* () { + const harness = yield* makeWakeHarness; + const now = yield* DateTime.now; + const awaitBackgroundTasks = harness.runtime.awaitBackgroundTasks; + if (awaitBackgroundTasks === undefined) { + throw new Error("Claude adapter runtime must expose awaitBackgroundTasks."); + } + const subagentStarted = (input: { + readonly taskId: string; + readonly toolUseId: string; + readonly backgrounded: boolean; + readonly uuid: string; + }) => + claudeSdkFrame({ + type: "system", + subtype: "task_started", + task_id: input.taskId, + tool_use_id: input.toolUseId, + description: input.taskId, + subagent_type: "general-purpose", + is_backgrounded: input.backgrounded, + task_type: "local_agent", + prompt: "Run a shell in the background.", + uuid: input.uuid, + session_id: WAKE_NATIVE_SESSION, + }); + + yield* harness.runtime.startTurn( + makeClaudeTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now, + attemptId: RunAttemptId.make("attempt-claude-foreground-owner"), + text: "Delegate background shells.", + attachments: [], + }), + ); + for (const frame of [ + subagentStarted({ + taskId: "agent-foreground", + toolUseId: "toolu_agent_foreground", + backgrounded: false, + uuid: "00000000-0000-4000-8000-000000000311", + }), + subagentStarted({ + taskId: "agent-background", + toolUseId: "toolu_agent_background", + backgrounded: true, + uuid: "00000000-0000-4000-8000-000000000312", + }), + claudeSdkFrame({ + type: "system", + subtype: "background_tasks_changed", + tasks: [ + { task_id: "root-shell", task_type: "local_bash", description: "root" }, + { + task_id: "foreground-owned-shell", + task_type: "local_bash", + description: "foreground owned", + parent_task_id: "agent-foreground", + }, + { + task_id: "background-owned-shell", + task_type: "local_bash", + description: "background owned", + parent_task_id: "agent-background", + }, + ], + uuid: "00000000-0000-4000-8000-000000000313", + session_id: WAKE_NATIVE_SESSION, + }), + ]) { + yield* Queue.offer(harness.sdkMessages, frame); + } + while (true) { + const { providerThread } = yield* Queue.take(harness.providerThreadReceipts); + if ((providerThread.pendingBackgroundTasks?.length ?? 0) === 3) break; + } + + // The foreground subagent returns its result inside the launching turn, so its + // own background work never reaches the root thread as a wake. + const marked = yield* awaitBackgroundTasks({ + providerThread: harness.providerThread, + taskIds: ["root-shell", "foreground-owned-shell", "background-owned-shell"], + awaited: true, + }); + assert.deepStrictEqual(marked.matched, ["root-shell", "background-owned-shell"]); + }).pipe( + Effect.provide( + Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), + ), + ), + ), + ); + it.effect("keeps a running Claude monitor typed after many newer monitors end", () => Effect.scoped( Effect.gen(function* () { From c367f8c91b5b09ef9627322d11a842f60eea0dae Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 25 Oct 2026 21:56:46 +0000 Subject: [PATCH 2/6] fix(server): Claude foreground subagents can no longer await background work t3_background_wait accepted background commands and Monitors owned by a foreground native subagent. Claude Code kills that work when the subagent returns, so the thread was held for a wake that never came. The adapter now only marks root work and work owned by a backgrounded subagent, and the injected instructions tell the agent to finish foreground work in-turn or move long work to a background subagent or delegate_task. Fixes #24956 Co-Authored-By: Claude Opus 5.5 --- .../Adapters/ClaudeAdapterV2.test.ts | 14 ++++++++++++++ .../orchestration-v2/Adapters/ClaudeAdapterV2.ts | 14 ++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index c886953d4b068..55bacfeb1e1b8 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -7613,6 +7613,20 @@ describe("ClaudeAdapterV2 background wake turns", () => { awaited: true, }); assert.deepStrictEqual(marked.matched, ["root-shell", "background-owned-shell"]); + while (true) { + const { providerThread } = yield* Queue.take(harness.providerThreadReceipts); + const tasks = providerThread.pendingBackgroundTasks ?? []; + if (!tasks.some((task) => task.awaited === true)) continue; + assert.deepStrictEqual( + tasks.map((task) => [task.taskId, task.awaited === true]), + [ + ["root-shell", true], + ["foreground-owned-shell", false], + ["background-owned-shell", true], + ], + ); + break; + } }).pipe( Effect.provide( Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index e15d52e07f1c3..14fa31b54bcf1 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -1327,7 +1327,7 @@ const CLAUDE_BACKGROUND_WAIT_INSTRUCTIONS = ` ### Background work you wait on -To wait for a background command or Monitor you started (tests, a build), call \`t3_background_wait\` with its task ID before you end your turn. Do not mark work you leave running, or PRs. T3 MCP tools are deferred: load one with ToolSearch before you call it. +To wait for a background command or Monitor you started (tests, a build), call \`t3_background_wait\` with its task ID before you end your turn. Do not mark work you leave running, or PRs. Background work started by a foreground subagent stops when that subagent returns, so it cannot be waited on and is reported unmatched: a foreground subagent must finish its work before it returns, and long work belongs in a background subagent or \`delegate_task\`. T3 MCP tools are deferred: load one with ToolSearch before you call it. `; export function makeClaudeQueryOptions(input: { @@ -10584,7 +10584,17 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( yield* Ref.get(pendingBackgroundTasksByNativeThread), nativeThreadId, ); - const matched = [...new Set(taskIds)].filter((taskId) => roster.has(taskId)); + const backgroundedSubagents = yield* Ref.get(backgroundedSubagentTaskIds); + // A foreground subagent's background work dies with its turn and never wakes the root. + const matched = [...new Set(taskIds)].filter((taskId) => { + const task = roster.get(taskId); + if (task === undefined) return false; + return ( + !awaited || + task.parentTaskId === undefined || + backgroundedSubagents.has(task.parentTaskId) + ); + }); if (matched.length === 0) return { matched }; if (awaited) { yield* addTaskIdsToNativeThreadSet( From ad750f8002e1dd401cc7acb37f5a2854085e999b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 25 Oct 2026 21:57:17 +0000 Subject: [PATCH 3/6] fix(server): direct long native agent work through delegation --- apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 14fa31b54bcf1..1c21ef4bb2c16 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -1327,7 +1327,7 @@ const CLAUDE_BACKGROUND_WAIT_INSTRUCTIONS = ` ### Background work you wait on -To wait for a background command or Monitor you started (tests, a build), call \`t3_background_wait\` with its task ID before you end your turn. Do not mark work you leave running, or PRs. Background work started by a foreground subagent stops when that subagent returns, so it cannot be waited on and is reported unmatched: a foreground subagent must finish its work before it returns, and long work belongs in a background subagent or \`delegate_task\`. T3 MCP tools are deferred: load one with ToolSearch before you call it. +To wait for a background command or Monitor you started (tests, a build), call \`t3_background_wait\` with its task ID before you end your turn. Do not mark work you leave running, or PRs. Background work started by a foreground subagent stops when that subagent returns, so it cannot be waited on and is reported unmatched: a foreground subagent must finish its work before it returns, and long work belongs in \`delegate_task\`. T3 MCP tools are deferred: load one with ToolSearch before you call it. `; export function makeClaudeQueryOptions(input: { From b8ccd0c4cd313ceecf524dbd443e85eb0990a3cc Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 30 Oct 2026 07:53:27 +0000 Subject: [PATCH 4/6] fix(server): fit foreground subagent wait note into the context budget Co-Authored-By: Claude Opus 5.5 --- apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts | 2 +- apps/server/src/provider/agentContextBudget.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 8b603dde9c7cb..5d8c18f952673 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -1439,7 +1439,7 @@ const CLAUDE_BACKGROUND_WAIT_INSTRUCTIONS = ` ### Background work you wait on -To wait for a background command or Monitor you started (tests, a build), call \`t3_background_wait\` with its task ID before you end your turn. Do not mark work you leave running, or PRs. Background work started by a foreground subagent stops when that subagent returns, so it cannot be waited on and is reported unmatched: a foreground subagent must finish its work before it returns, and long work belongs in \`delegate_task\`. T3 MCP tools are deferred: load one with ToolSearch before you call it. +To wait for a background command or Monitor you started (tests, a build), call \`t3_background_wait\` with its task ID before you end your turn. Do not mark work you leave running, or PRs. A foreground subagent's background work stops when it returns, so it cannot be waited on; finish it before returning. T3 MCP tools are deferred: load one with ToolSearch before you call it. `; // Native subagents start from their task, not this session's system prompt. diff --git a/apps/server/src/provider/agentContextBudget.json b/apps/server/src/provider/agentContextBudget.json index 1711e148d4ca4..9cc5a5075fc70 100644 --- a/apps/server/src/provider/agentContextBudget.json +++ b/apps/server/src/provider/agentContextBudget.json @@ -1,5 +1,5 @@ { - "claudeSystemAppendBytes": 5135, + "claudeSystemAppendBytes": 5253, "codexAdditionalContextBytes": 6865, "mcpToolListJsonBytes": 46007 } From 470adaef17bc7c25df305ca862734ab94572ab51 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 30 Oct 2026 07:57:57 +0000 Subject: [PATCH 5/6] fix(server): keep agent context at main's size for the foreground wait fix The unmatched result already tells a foreground subagent its wait was not accepted, so the extra instruction sentence is not needed. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts | 2 +- apps/server/src/provider/agentContextBudget.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 5d8c18f952673..ce6905a743b4e 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -1439,7 +1439,7 @@ const CLAUDE_BACKGROUND_WAIT_INSTRUCTIONS = ` ### Background work you wait on -To wait for a background command or Monitor you started (tests, a build), call \`t3_background_wait\` with its task ID before you end your turn. Do not mark work you leave running, or PRs. A foreground subagent's background work stops when it returns, so it cannot be waited on; finish it before returning. T3 MCP tools are deferred: load one with ToolSearch before you call it. +To wait for a background command or Monitor you started (tests, a build), call \`t3_background_wait\` with its task ID before you end your turn. Do not mark work you leave running, or PRs. T3 MCP tools are deferred: load one with ToolSearch before you call it. `; // Native subagents start from their task, not this session's system prompt. diff --git a/apps/server/src/provider/agentContextBudget.json b/apps/server/src/provider/agentContextBudget.json index 9cc5a5075fc70..1711e148d4ca4 100644 --- a/apps/server/src/provider/agentContextBudget.json +++ b/apps/server/src/provider/agentContextBudget.json @@ -1,5 +1,5 @@ { - "claudeSystemAppendBytes": 5253, + "claudeSystemAppendBytes": 5135, "codexAdditionalContextBytes": 6865, "mcpToolListJsonBytes": 46007 } From ec268f1a9c5b47435c174a8b9b9a44e77868dfb0 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 01:12:21 +0000 Subject: [PATCH 6/6] test(server): tighten foreground subagent wait regression test AI-assisted; verified with the tests and tools listed above. Co-Authored-By: Claude Opus 5.5 --- .../Adapters/ClaudeAdapterV2.test.ts | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 4e1a87696b8c4..6193426e544d4 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -10405,31 +10405,20 @@ describe("ClaudeAdapterV2 background wake turns", () => { if ((providerThread.pendingBackgroundTasks?.length ?? 0) === 3) break; } - // The foreground subagent returns its result inside the launching turn, so its - // own background work never reaches the root thread as a wake. const marked = yield* awaitBackgroundTasks({ providerThread: harness.providerThread, taskIds: ["root-shell", "foreground-owned-shell", "background-owned-shell"], awaited: true, }); assert.deepStrictEqual(marked.matched, ["root-shell", "background-owned-shell"]); - while (true) { - const { providerThread } = yield* Queue.take(harness.providerThreadReceipts); - const tasks = providerThread.pendingBackgroundTasks ?? []; - if (!tasks.some((task) => task.awaited === true)) continue; - assert.deepStrictEqual( - tasks.map((task) => [task.taskId, task.awaited === true]), - [ - ["root-shell", true], - ["foreground-owned-shell", false], - ["background-owned-shell", true], - ], - ); - break; - } }).pipe( Effect.provide( - Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), + Layer.mergeAll( + IdAllocator.layer, + McpProviderSessions.layer, + NodeServices.layer, + NodeCrypto.layer, + ), ), ), ),