diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 9f2e601afa382..12bdbb2ebbd41 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -11529,6 +11529,67 @@ describe("ClaudeAdapterV2 background wake turns", () => { ), ); + it.effect("keeps a foreground fork running past the placeholder result for its own launch", () => + Effect.scoped( + Effect.gen(function* () { + const harness = yield* makeWakeHarness; + yield* harness.runtime.startTurn( + makeClaudeTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now: yield* DateTime.now, + attemptId: RunAttemptId.make("attempt-claude-foreground-fork"), + text: "Fork this.", + attachments: [], + }), + ); + yield* harness.offerAndWait( + makeSubagentTaskStartedFrame({ + taskId: "agent-fork", + toolUseId: "toolu-fork", + uuid: "00000000-0000-4000-8000-000000000901", + }), + ); + // Claude Code opens a foreground fork's own stream with a placeholder result for its launch. + yield* harness.offerAndWait( + claudeSdkFrame({ + type: "user", + message: { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "toolu-fork", + content: "Fork started — that fork is you.", + }, + ], + }, + parent_tool_use_id: "toolu-fork", + uuid: "00000000-0000-4000-8000-000000000902", + session_id: WAKE_NATIVE_SESSION, + }), + ); + yield* harness.offerAndWait( + makeSubagentNotificationFrame({ + taskId: "agent-fork", + toolUseId: "toolu-fork", + summary: "FORK-DONE", + uuid: "00000000-0000-4000-8000-000000000903", + }), + ); + let settled = yield* Queue.take(harness.subagentReceipts); + while (settled.subagent.status === "running") { + settled = yield* Queue.take(harness.subagentReceipts); + } + assert.equal(settled.subagent.result, "FORK-DONE"); + }).pipe( + Effect.provide( + Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), + ), + ), + ), + ); + it.effect("releases the idle pin when a post-settle subagent stops without completing", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 5c676d684f686..d39f7fc75aa64 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -8819,6 +8819,8 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( } for (const { toolResult, output } of claudeToolResultEntriesFromMessage(message)) { + // A foreground fork's stream opens with a placeholder result for its own launch call. + if (parentToolUseIdFromSdkMessage(message) === toolResult.tool_use_id) continue; const subagent = yield* resolveSubagentByToolUseId(context, toolResult.tool_use_id); const runHandles = parseClaudeWorkflowRunHandles(claudeNativeToolOutputValue(output)); if (subagent !== undefined && runHandles !== undefined) {