diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index ed3a64c64ef9..fac9cb3b7640 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -2463,8 +2463,6 @@ interface ActiveClaudeSubagent { // SendMessage that resumes the subagent. A new one means a new prompt. readonly runToolUseId: string | null; nextChildItemOrdinal: number; - progressItemOrdinal: number | null; - progressStartedAt: DateTime.Utc | null; resultItemOrdinal: number | null; // The subagent's latest assistant message routed into its child thread, // accumulated across the per-content-block snapshots that share one native @@ -3552,8 +3550,6 @@ export function makeClaudeAdapterV2( ? (input.toolUseId ?? null) : (resumeToolUseId ?? existingSubagent.runToolUseId), nextChildItemOrdinal: existingSubagent?.nextChildItemOrdinal ?? 100, - progressItemOrdinal: existingSubagent?.progressItemOrdinal ?? null, - progressStartedAt: existingSubagent?.progressStartedAt ?? null, resultItemOrdinal: existingSubagent?.resultItemOrdinal ?? null, // Every task_started begins a new run of the subagent (including a // resume that bufferWakeMessage already pre-opened), so text from @@ -3751,50 +3747,6 @@ export function makeClaudeAdapterV2( }, }); - const progress = task.progress?.trim(); - if ( - progress !== undefined && - progress.length > 0 && - (input.progress !== undefined || (lifecycleChanged && input.status !== "running")) - ) { - const progressNativeItemId = `${nativeItemId}:progress`; - const progressItemOrdinal = - subagent.progressItemOrdinal ?? ++subagent.nextChildItemOrdinal; - const progressStartedAt = subagent.progressStartedAt ?? now; - subagent.progressItemOrdinal = progressItemOrdinal; - subagent.progressStartedAt = progressStartedAt; - yield* emitProviderEvent({ - type: "turn_item.updated", - driver: CLAUDE_PROVIDER, - turnItem: { - id: idAllocator.derive.turnItemFromProviderItem({ - driver: CLAUDE_PROVIDER, - nativeItemId: progressNativeItemId, - }), - threadId: childThreadId, - runId: null, - nodeId: childRootNodeId, - providerThreadId: null, - providerTurnId: null, - nativeItemRef: { - driver: CLAUDE_PROVIDER, - nativeId: progressNativeItemId, - strength: "strong", - }, - parentItemId: null, - ordinal: progressItemOrdinal, - status: input.status, - title: "Subagent progress", - startedAt: progressStartedAt, - completedAt: input.status === "running" ? null : now, - updatedAt: now, - type: "reasoning", - text: progress, - streaming: input.status === "running", - }, - }); - } - // A completed subagent's result is normally its final assistant // message, which is already in the child thread when its text was // routed there. Failures and cancellations always get the message. diff --git a/apps/server/src/orchestration-v2/FoundationPersistence.test.ts b/apps/server/src/orchestration-v2/FoundationPersistence.test.ts index 92ce52358197..cf27490cf540 100644 --- a/apps/server/src/orchestration-v2/FoundationPersistence.test.ts +++ b/apps/server/src/orchestration-v2/FoundationPersistence.test.ts @@ -2903,8 +2903,7 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => { payload: node({ id: childRootId, threadId: childId, runId: null, kind: "root_turn" }), }, { - // Claude's live "Subagent progress" item in the child, as the - // adapter writes it while task_progress frames arrive. + // The subagent's live thinking in the child, still streaming. id: EventId.make("event:foundation-native-subagent:child-progress"), type: "turn-item.updated", threadId: childId, @@ -2922,11 +2921,11 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => { ordinal: 101, type: "reasoning", status: "running", - title: "Subagent progress", + title: "Thinking", startedAt: now, completedAt: null, updatedAt: now, - text: "Running git diff --stat", + text: "Checking the diff.", streaming: true, }, }, diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/claude_background_subagent_lifecycle/output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/claude_background_subagent_lifecycle/output.ts index 8f2fdd4f9f1b..3471d8839418 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/claude_background_subagent_lifecycle/output.ts +++ b/apps/server/src/orchestration-v2/testkit/fixtures/claude_background_subagent_lifecycle/output.ts @@ -200,6 +200,20 @@ export function assertClaudeBackgroundSubagentLifecycleOutput( "subagent thinking leaked into the parent thread", ); + // A finished subagent's child thread holds only its own settled work. + // Claude's task_progress summary ("Running ") stays on the parent's + // subagent card; in the child it read as a step still running. + assert.equal(agentB?.progress, "Running Wait 90 seconds then print B_DONE"); + for (const child of [agentAChild, agentBChild]) { + for (const item of child?.turnItems ?? []) { + assert.notInclude(["pending", "running", "waiting"], item.status, item.id); + assert.isFalse( + item.type === "reasoning" && item.title !== "Thinking", + `${item.id} is not the subagent's own thinking`, + ); + } + } + // Subagents appear in background_tasks_changed but never on the roster. assert.isFalse( result.domainEvents.some( diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/subagent/claude_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/subagent/claude_output.ts index 1f5585a79381..96358cc449db 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/subagent/claude_output.ts +++ b/apps/server/src/orchestration-v2/testkit/fixtures/subagent/claude_output.ts @@ -92,12 +92,7 @@ export function assertClaudeSubagentOutput( assert.lengthOf(childProjection.providerThreads, 0); assert.lengthOf(childProjection.providerTurns, 0); assertExecutionNodeKinds(childProjection, ["root_turn", "tool_call"]); - assertTurnItemTypes(childProjection, [ - "user_message", - "reasoning", - "dynamic_tool", - "assistant_message", - ]); + assertTurnItemTypes(childProjection, ["user_message", "dynamic_tool", "assistant_message"]); assertUserMessagesInclude(childProjection, [subagent.prompt]); assert.isTrue( childProjection.turnItems.some( @@ -108,20 +103,13 @@ export function assertClaudeSubagentOutput( ), `child thread ${subagent.childThreadId} must contain the subagent response`, ); - const progressItems = childProjection.turnItems.filter((item) => item.type === "reasoning"); - assert.lengthOf( - progressItems, - 1, - `child thread ${subagent.childThreadId} must coalesce progress into one item`, + // Progress stays on the subagent card; the child shows the work itself. + assert.isFalse( + childProjection.turnItems.some( + (item) => item.type === "reasoning" && item.text === expectedProgress, + ), + `child thread ${subagent.childThreadId} must not repeat the subagent's progress`, ); - const progressItem = progressItems[0]; - if (progressItem === undefined) { - throw new Error(`Missing progress item for subagent ${subagent.id}`); - } - assert.equal(progressItem.text, expectedProgress); - assert.equal(progressItem.status, "completed"); - assert.isFalse(progressItem.streaming); - assert.isNotNull(progressItem.completedAt); const parentItem = projection.turnItems.find( (item) => item.type === "subagent" && item.subagentId === subagent.id,