From 08f376f5012184bac4e677ac8e81a5106087c05c Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 18:11:40 +0200 Subject: [PATCH] fix(claude): show a background subagent's action log on its card Claude Code answers a background Agent call at launch and streams the child's frames afterwards. Since #6067 the sidechain tracker stopped emitting the parent card once the call had its result, so the card's log stayed empty. The tracker now rebuilds the card from the settled call, keeping its status, output, and label, and adds the child's action log. Closes #6157 --- .../claude/agent.background-subagent.test.ts | 1 + .../server/agent/providers/claude/agent.ts | 50 ++++++++++++------- .../providers/claude/sidechain-tracker.ts | 26 +++++++--- 3 files changed, 52 insertions(+), 25 deletions(-) diff --git a/packages/server/src/server/agent/providers/claude/agent.background-subagent.test.ts b/packages/server/src/server/agent/providers/claude/agent.background-subagent.test.ts index 630bdbf0a6..7c0beed569 100644 --- a/packages/server/src/server/agent/providers/claude/agent.background-subagent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.background-subagent.test.ts @@ -431,6 +431,7 @@ describe("background Claude subagents", () => { type: "sub_agent", subAgentType: "general-purpose", description: "Count files here", + log: "[Bash] ls -1", }, }); }); diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index aa60e40462..3cd1b9aa63 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -2094,18 +2094,29 @@ class ClaudeAgentSession implements AgentSession { getToolInput: (toolUseId) => this.toolUseCache.get(toolUseId)?.input ?? null, readWorkflowResult: readClaudeWorkflowResultFile, }); + /** + * Subagent tool calls Claude has already answered, with the input they were launched with. A + * background child is answered at launch and streams its frames afterwards. + */ + private readonly settledSubagentToolCalls = new Map< + string, + { item: Extract; input: AgentMetadata | null } + >(); private readonly sidechainTracker = new ClaudeSidechainTracker({ - getToolInput: (toolUseId) => this.toolUseCache.get(toolUseId)?.input ?? null, + getToolInput: (toolUseId) => + this.toolUseCache.get(toolUseId)?.input ?? + this.settledSubagentToolCalls.get(toolUseId)?.input ?? + null, // Releases that predate the task protocol announce nothing, so the tracker keeps deriving // identity and status from frames for them. Detecting the capability beats comparing version // strings: it reacts to what this session actually does. isDescriptorOwnedElsewhere: () => this.taskProtocolSource.isActive, - // The parent's tool call owns its card once its result arrives. A background child streams - // its frames after Claude has already answered the call, and re-emitting the card from them - // would replace the settled card with an unlabeled, running one. + // A background child streams its frames after Claude has already answered the call, so its + // card is rebuilt from the settled call: a running one would replace the settled, labeled card. needsSyntheticParentToolCard: (toolUseId) => - this.toolUseCache.has(toolUseId) && + (this.toolUseCache.has(toolUseId) || this.settledSubagentToolCalls.has(toolUseId)) && this.taskProtocolSource.needsSyntheticParentToolCard(toolUseId), + getSettledParentToolCall: (toolUseId) => this.settledSubagentToolCalls.get(toolUseId)?.item, }); private persistedHistory: PersistedTimelineEntry[] = []; private persistedProviderSubagentEvents: Extract< @@ -2711,6 +2722,7 @@ class ClaudeAgentSession implements AgentSession { this.cancelCurrentTurn = null; this.turnState = "idle"; this.sidechainTracker.clear(); + this.settledSubagentToolCalls.clear(); this.taskProtocolSource.reset(); this.input?.end(); this.query?.close?.(); @@ -5308,28 +5320,22 @@ class ClaudeAgentSession implements AgentSession { const { images, text } = splitClaudeToolResultImages(block.content); const output = this.buildToolOutput(text, block, entry); - if (block.is_error) { - this.pushToolCall( - mapClaudeFailedToolCall({ + const settledToolCall = block.is_error + ? mapClaudeFailedToolCall({ name: toolName, callId, input: entry?.input ?? null, output: output ?? null, error: { ...block, content: text }, - }), - items, - ); - } else { - this.pushToolCall( - mapClaudeCompletedToolCall({ + }) + : mapClaudeCompletedToolCall({ name: toolName, callId, input: entry?.input ?? null, output: output ?? null, - }), - items, - ); - } + }); + this.pushToolCall(settledToolCall, items); + this.rememberSettledSubagentToolCall(settledToolCall, entry); for (const image of images) { const imageItem = renderProviderImageOutputAsAssistantMarkdown(image, { @@ -5345,6 +5351,14 @@ class ClaudeAgentSession implements AgentSession { } } + private rememberSettledSubagentToolCall( + item: Extract | null, + entry: ToolUseCacheEntry | undefined, + ): void { + if (!item || !this.taskProtocolSource.resolveSubagentId(item.callId)) return; + this.settledSubagentToolCalls.set(item.callId, { item, input: entry?.input ?? null }); + } + private buildToolOutput( content: unknown, block: ClaudeContentChunk, diff --git a/packages/server/src/server/agent/providers/claude/sidechain-tracker.ts b/packages/server/src/server/agent/providers/claude/sidechain-tracker.ts index 6973d1db5d..da6faafa90 100644 --- a/packages/server/src/server/agent/providers/claude/sidechain-tracker.ts +++ b/packages/server/src/server/agent/providers/claude/sidechain-tracker.ts @@ -14,6 +14,8 @@ interface ClaudeContentChunk { [key: string]: unknown; } +type ClaudeToolCallItem = Extract; + interface SubAgentActionEntry { index: number; toolName: string; @@ -60,6 +62,7 @@ export class ClaudeSidechainTracker { private readonly getToolInput: (toolUseId: string) => AgentMetadata | null | undefined; private readonly isDescriptorOwnedElsewhere: () => boolean; private readonly needsSyntheticParentToolCard: (toolUseId: string) => boolean; + private readonly getSettledParentToolCall: (toolUseId: string) => ClaudeToolCallItem | undefined; constructor(input: { getToolInput: (toolUseId: string) => AgentMetadata | null | undefined; @@ -70,10 +73,17 @@ export class ClaudeSidechainTracker { */ isDescriptorOwnedElsewhere?: () => boolean; needsSyntheticParentToolCard?: (toolUseId: string) => boolean; + /** + * The parent's tool call once Claude has answered it. A background child streams its frames + * after that answer, so its card keeps the settled status and output and gains the child's + * action log. + */ + getSettledParentToolCall?: (toolUseId: string) => ClaudeToolCallItem | undefined; }) { this.getToolInput = input.getToolInput; this.isDescriptorOwnedElsewhere = input.isDescriptorOwnedElsewhere ?? (() => false); this.needsSyntheticParentToolCard = input.needsSyntheticParentToolCard ?? (() => true); + this.getSettledParentToolCall = input.getSettledParentToolCall ?? (() => undefined); } handleMessage(message: SDKMessage, parentToolUseId: string): AgentStreamEvent[] { @@ -115,17 +125,19 @@ export class ClaudeSidechainTracker { return []; } - const toolCall = mapClaudeRunningToolCall({ - name: "Task", - callId: parentToolUseId, - input: null, - output: null, - }); + const toolCall = + this.getSettledParentToolCall(parentToolUseId) ?? + mapClaudeRunningToolCall({ + name: "Task", + callId: parentToolUseId, + input: null, + output: null, + }); if (!toolCall) { return []; } - const detail: Extract["detail"] = { + const detail: ClaudeToolCallItem["detail"] = { type: "sub_agent", ...(state.subAgentType ? { subAgentType: state.subAgentType } : {}), ...(state.description ? { description: state.description } : {}),