Skip to content

Do not mark a denied subagent tool call as a subagent - #334549

Open
Ryan Ewen (RyanEwen) wants to merge 2 commits into
microsoft:mainfrom
RyanEwen:fix/denied-subagent-no-phantom-chat
Open

Do not mark a denied subagent tool call as a subagent#334549
Ryan Ewen (RyanEwen) wants to merge 2 commits into
microsoft:mainfrom
RyanEwen:fix/denied-subagent-no-phantom-chat

Conversation

@RyanEwen

@RyanEwen Ryan Ewen (RyanEwen) commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

A denied Agent or Task call still gets a subagent marker attached to its result, so on restore the host registers a subagent chat whose transcript can never exist.

What happens

toolKind is derived from the tool name, with no regard for the outcome, and _attachToolResult pushes ToolResultContentType.Subagent unconditionally. On restore, _registerRestoredSubagentSummaries scans replayed turns for that marker and registers a chat with a lazy resolver, and the resolver throws Subagent transcript is not available yet forever, because nothing was ever spawned.

Since #334312 that is no longer fatal to the session, but the phantom chat is still registered, still fails to subscribe, and still shows in the session's chat list as a subagent that never produces anything.

How it was hit

Auto mode could not classify an Agent call and denied it:

"...temporarily unavailable, so auto mode cannot determine the safety of Agent right now..."
"toolDenialKind": "automode-unavailable"

The SDK transcript for that session is 47 lines with zero isSidechain entries, confirming no subagent ran. Any denial reaches this the same way, so a permission rule or a user rejecting the call does too.

The change

is_error alone is not the right test: it also covers a subagent that started and then failed, and those children have real transcripts that must still be linked on restore.

The SDK stamps agentId: <hex> on the tool_result of a call that actually spawned, which is what scanTranscriptForAgentIds already keys on through the exported SUBAGENT_ID_SUFFIX_REGEX. A pre-spawn denial is therefore the case with an error and no agentId:

const spawned = !isError || SUBAGENT_ID_SUFFIX_REGEX.test(resultText);
if (isSubagent && spawned) {

resultText is already computed immediately above for the past-tense message, so nothing new is derived.

Tests

  • Fixture 5c: a denied Agent tool_use produces no subagent marker asserts the call is still recognised as a subagent by name, that success is false, and that no marker is attached.
  • Fixture 5d: an Agent that spawned and then failed keeps its subagent marker is the failed-after-spawn case, and fails against an is_error-only condition.

Fixtures 5 and 5b continue to pass, so successful spawns are unchanged. The suite is 30 passing. No new type errors.

AI disclosure: this comment and the related code were written with the assistance of AI.

Subagent-ness is taken from the tool name, so a denied `Agent` or `Task`
call still has a subagent marker attached to its result. On restore that
marker registers a subagent chat whose transcript can never exist, because
nothing was ever spawned.

`is_error` is already computed immediately above the push and was simply not
consulted. Successful calls are unaffected.
@vs-code-engineering

Copy link
Copy Markdown
Contributor

📬 CODENOTIFY

The following users are being notified based on files changed in this PR:

TylerLeonhardt

Matched files:

  • src/vs/platform/agentHost/node/claude/claudeReplayMapper.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

is_error also covers spawned subagents that later fail, potentially hiding valid transcripts; denial-specific handling and failure coverage are needed.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Prevents denied Claude subagent calls from creating phantom restored chats.

Changes:

  • Suppresses subagent markers for errored tool results.
  • Adds regression coverage for denied Agent calls.
File summaries
File Description
src/vs/platform/agentHost/test/node/claudeReplayMapper.test.ts Adds denied-call replay coverage.
src/vs/platform/agentHost/node/claude/claudeReplayMapper.ts Conditionally attaches restored subagent markers; the condition is currently too broad.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/vs/platform/agentHost/node/claude/claudeReplayMapper.ts Outdated
`is_error` also covers a subagent that started and then failed, and those
children have real transcripts. Keying on it alone hid every failed
subagent on restore.

The SDK stamps `agentId: <hex>` on the tool_result of a call that actually
spawned, which is what `scanTranscriptForAgentIds` already keys on, so a
pre-spawn denial is the case with an error and no agentId.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants