From 5e1d737ddf76e2a8e541b0b6f109d5e29d28f5e0 Mon Sep 17 00:00:00 2001 From: issacboy123 Date: Thu, 27 Aug 2026 16:53:47 -0700 Subject: [PATCH 1/2] fix(coding-agent): bind spawn origin to selected branch --- packages/coding-agent/CHANGELOG.md | 1 + .../src/core/subagents/controller.ts | 30 ++--- .../test/suite/subagent-registration.test.ts | 107 +++++++++++++++--- 3 files changed, 111 insertions(+), 27 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index b63185600..9072bd67c 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -21,6 +21,7 @@ - Fixed right-click not pasting clipboard text in fullscreen mode on Windows. - Fixed sub-agent spawning bypassing execution-mutating parent extension hooks by rejecting incompatible parent policies before child creation. - Fixed delegated sub-agent result and escalation guidance disappearing from custom, extension-returned, or tool-filtered root prompts. +- Fixed sub-agent spawn provenance selecting stale or abandoned tool calls when identifiers were reused. ## [0.84.0] - 2026-08-06 diff --git a/packages/coding-agent/src/core/subagents/controller.ts b/packages/coding-agent/src/core/subagents/controller.ts index f9ae388f3..6864f713c 100644 --- a/packages/coding-agent/src/core/subagents/controller.ts +++ b/packages/coding-agent/src/core/subagents/controller.ts @@ -136,23 +136,27 @@ function captureSpawnOrigin(parent: AgentSession, toolCallId: string): SubagentS }); } - const matchingEntries = parent.sessionManager - .getEntries() - .filter( - (entry) => - entry.type === "message" && - entry.message.role === "assistant" && - entry.message.content.some( - (content) => content.type === "toolCall" && content.id === toolCallId && content.name === "spawn_agent", - ), - ); - if (matchingEntries.length !== 1) { + const branch = parent.sessionManager.getBranch(); + let assistantEntry: (typeof branch)[number] | undefined; + for (let index = branch.length - 1; index >= 0; index -= 1) { + const entry = branch[index]; + if ( + entry.type === "message" && + entry.message.role === "assistant" && + entry.message.content.some( + (content) => content.type === "toolCall" && content.id === toolCallId && content.name === "spawn_agent", + ) + ) { + assistantEntry = entry; + break; + } + } + if (!assistantEntry) { throw new Error( - `Invariant violation: active spawn_agent call ${toolCallId} matched ${matchingEntries.length} persisted assistant entries`, + `Invariant violation: active spawn_agent call ${toolCallId} matched 0 persisted assistant entries`, ); } - const assistantEntry = matchingEntries[0]; return Object.freeze({ type: "agent_tool_call", toolCallId, diff --git a/packages/coding-agent/test/suite/subagent-registration.test.ts b/packages/coding-agent/test/suite/subagent-registration.test.ts index 5199e9448..3c0f0d3da 100644 --- a/packages/coding-agent/test/suite/subagent-registration.test.ts +++ b/packages/coding-agent/test/suite/subagent-registration.test.ts @@ -134,6 +134,35 @@ function getActiveTool(session: AgentSession, name: string): AgentTool { return tool; } +function appendAssistantToolCall(sessionManager: SessionManager, toolName: string, toolCallId: string): string { + return sessionManager.appendMessage( + fauxAssistantMessage(fauxToolCall(toolName, {}, { id: toolCallId }), { stopReason: "toolUse" }), + ); +} + +async function executeActiveSpawn(session: AgentSession, toolCallId: string, name: string): Promise { + const internalState = session as unknown as { _isAgentRunActive: boolean }; + internalState._isAgentRunActive = true; + try { + await getActiveTool(session, "spawn_agent").execute(toolCallId, { name, task: `${name} task` }); + } finally { + internalState._isAgentRunActive = false; + } +} + +function captureSubagentSessionManagerRequests(): SubagentSessionManagerRequest[] { + const requests: SubagentSessionManagerRequest[] = []; + const originalCreate = RootLocalEphemeralSubagentSessionManagerFactory.prototype.create; + vi.spyOn(RootLocalEphemeralSubagentSessionManagerFactory.prototype, "create").mockImplementation(function ( + this: RootLocalEphemeralSubagentSessionManagerFactory, + request, + ) { + requests.push(request); + return originalCreate.call(this, request); + }); + return requests; +} + describe("root subagent tool registration", () => { const harnesses: Harness[] = []; const sessions: AgentSession[] = []; @@ -576,23 +605,73 @@ describe("root subagent tool registration", () => { } }); + it("binds reused tool-call identifiers to the newest selected-branch spawn", async () => { + const harness = await setupHarness(); + harness.setResponses([fauxAssistantMessage("child complete")]); + const requests = captureSubagentSessionManagerRequests(); + const { session } = await createRoot(harness); + appendAssistantToolCall(session.sessionManager, "spawn_agent", "reused-call"); + const newestEntryId = appendAssistantToolCall(session.sessionManager, "spawn_agent", "reused-call"); + + await executeActiveSpawn(session, "reused-call", "worker"); + + expect(requests).toHaveLength(1); + expect(requests[0]?.origin).toMatchObject({ + type: "agent_tool_call", + toolCallId: "reused-call", + assistantEntryId: newestEntryId, + }); + }); + + it("ignores newer calls to other tools that reuse the spawn identifier", async () => { + const harness = await setupHarness(); + harness.setResponses([fauxAssistantMessage("child complete")]); + const requests = captureSubagentSessionManagerRequests(); + const { session } = await createRoot(harness); + const spawnEntryId = appendAssistantToolCall(session.sessionManager, "spawn_agent", "shared-call"); + appendAssistantToolCall(session.sessionManager, "agents", "shared-call"); + + await executeActiveSpawn(session, "shared-call", "worker"); + + expect(requests).toHaveLength(1); + expect(requests[0]?.origin).toMatchObject({ + type: "agent_tool_call", + toolCallId: "shared-call", + assistantEntryId: spawnEntryId, + }); + }); + + it("ignores matching spawn calls on abandoned branches", async () => { + const harness = await setupHarness(); + harness.setResponses([fauxAssistantMessage("child complete")]); + const requests = captureSubagentSessionManagerRequests(); + const { session } = await createRoot(harness); + const branchPointId = session.sessionManager.appendCustomEntry("branch-point"); + appendAssistantToolCall(session.sessionManager, "spawn_agent", "branched-call"); + session.sessionManager.branch(branchPointId); + const selectedEntryId = appendAssistantToolCall(session.sessionManager, "spawn_agent", "branched-call"); + + await executeActiveSpawn(session, "branched-call", "worker"); + + expect(requests).toHaveLength(1); + expect(requests[0]?.origin).toMatchObject({ + type: "agent_tool_call", + toolCallId: "branched-call", + assistantEntryId: selectedEntryId, + }); + }); + it("rejects unmatched spawn calls during an active agent run", async () => { const harness = await setupHarness(); const { session } = await createRoot(harness); - const internalState = session as unknown as { _isAgentRunActive: boolean }; - internalState._isAgentRunActive = true; - try { - await expect( - getActiveTool(session, "spawn_agent").execute("missing-call", { - name: "worker", - task: "task", - }), - ).rejects.toThrow( - "Invariant violation: active spawn_agent call missing-call matched 0 persisted assistant entries", - ); - } finally { - internalState._isAgentRunActive = false; - } + const branchPointId = session.sessionManager.appendCustomEntry("branch-point"); + appendAssistantToolCall(session.sessionManager, "spawn_agent", "missing-call"); + session.sessionManager.branch(branchPointId); + appendAssistantToolCall(session.sessionManager, "agents", "missing-call"); + + await expect(executeActiveSpawn(session, "missing-call", "worker")).rejects.toThrow( + "Invariant violation: active spawn_agent call missing-call matched 0 persisted assistant entries", + ); }); it("applies configured concurrency and captures parent state when spawning", async () => { From 215e46dd68cb7a824feee22e44153c22995be476 Mon Sep 17 00:00:00 2001 From: Jay Yu Date: Fri, 28 Aug 2026 23:52:26 -0400 Subject: [PATCH 2/2] docs(coding-agent): remove changelog update --- packages/coding-agent/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 9072bd67c..b63185600 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -21,7 +21,6 @@ - Fixed right-click not pasting clipboard text in fullscreen mode on Windows. - Fixed sub-agent spawning bypassing execution-mutating parent extension hooks by rejecting incompatible parent policies before child creation. - Fixed delegated sub-agent result and escalation guidance disappearing from custom, extension-returned, or tool-filtered root prompts. -- Fixed sub-agent spawn provenance selecting stale or abandoned tool calls when identifiers were reused. ## [0.84.0] - 2026-08-06