Repository navigation
Stop delivering steering messages the user removed from the queue - #342370
Merged
Karthik Nadig (karthiknadig) merged 2 commits intoOct 18, 2026
Merged
Karthik Nadig (karthiknadig) merged 2 commits into
Karthik Nadig (karthiknadig) merged 2 commits into
Conversation
Removing a pending steering message during an Agent Host turn was refused by the host, and nothing withdrew the message from the Copilot SDK, so it was still delivered as its own turn. Let providers that can withdraw steering accept the removal, and have Copilot withdraw it from the SDK steering queue (or skip sending it if it is still being prepared). A message the model already received is kept and still shown as its own turn. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
An echo arriving during withdrawal can recreate a removed identical-text request and leave the consumed message queued.
1 open issue
What changed in this PR
Adds provider-supported withdrawal of removed steering messages, addressing the removal scenario in #336466 while leaving editing restrictions separate.
Changes:
- Allows mid-turn removal for providers with withdrawal support.
- Withdraws Copilot SDK messages while preserving already-delivered messages in the transcript.
- Adds regression tests and documents the behavior.
| File | Description |
|---|---|
| src/vs/platform/agentHost/test/node/mockAgent.ts | Adds optional withdrawal mock. |
| src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts | Tests preparation, withdrawal, and already-delivered messages. |
| src/vs/platform/agentHost/test/node/chatContributions.test.ts | Tests client-only withdrawal dispatch. |
| src/vs/platform/agentHost/test/node/agentService.test.ts | Tests provider-supported mid-turn removal. |
| src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts | Tracks SDK sends and implements withdrawal. |
| src/vs/platform/agentHost/node/copilot/copilotAgent.ts | Forwards withdrawal to the chat session. |
| src/vs/platform/agentHost/node/chatContributions/queueDrain/queueDrainContribution.ts | Requests withdrawal for client removals. |
| src/vs/platform/agentHost/node/agentService.ts | Gates removal on provider support. |
| src/vs/platform/agentHost/common/agent.ts | Defines the optional withdrawal contract. |
| src/vs/platform/agentHost/AGENTS.md | Documents steering withdrawal behavior. |
Review effort: Balanced
When two steering messages had the same text and one was being withdrawn, the echo for the other one could be matched to the withdrawn message. That showed the removed message as a turn and left the delivered one pending. The SDK echo carries the id that send returned, so prefer an exact id match and only fall back to matching by text for messages without a known id. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Karthik Nadig (karthiknadig)
marked this pull request as ready for review
October 18, 2026 19:09
Karthik Nadig (karthiknadig)
enabled auto-merge (squash)
October 18, 2026 19:09
roblourens
approved these changes
Oct 18, 2026
Karthik Nadig (karthiknadig)
deleted the
karthiknadig/bug-swarm/withdraw-removed-steering
branch
October 18, 2026 23:03
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Refs #336466
Problem
In an Agent Host chat with the Copilot agent (the chat view or the Agents window), you can send a steering message while the agent works, then remove it with Remove from Queue. The removal does not stick: the agent still receives the message, and it appears in the transcript as its own request.
Root cause
Two layers let a removed steering message through.
AgentServicerejects a client'sChatPendingMessageRemovedfor a steering message while a turn is active ("Cannot change a steering message while it is being processed"). No provider could take back a message it had already been given, so the message stays queued on the host: agentService.ts#L11101-L11107.CopilotAgentSession.sendSteeringsends the message withmode: 'immediate'and discards the message ID thatsend()returns. It also keeps the entry it uses to recognize the SDK'suser.messageecho (copilotAgentSession.ts#L5587-L5627). When the echo arrives,_takeMatchingPendingSteeringstill matches the text and starts a turn for it (#L7853-L7855, #L2320-L2340). The SDK providessession.rpc.queue.withdrawMessage({ messageId, expectedPrompt })for this case, but nothing calls it.Fix
IAgentgets an optionalwithdrawSteeringMessage(chat, messageId). The host accepts removal of an in-flight steering message only when the session's provider implements it. Codex and Claude do not implement it, so they keep today's rejection. Editing an in-flight steering message is still rejected for every provider.withdrawSteeringMessage. Removals the host makes itself, after the agent consumed a message or on cancel, do not trigger a withdrawal.CopilotAgentSessionkeeps the SDK message ID and the exact prompt it sent.withdrawSteering:rpc.queue.withdrawMessageand forgets the message only when the SDK confirms that it removed it;removed: false). The message still appears as its own turn, so the transcript matches what the model saw, and the agent host logs that it could not be withdrawn.user.messageecho carries the message ID thatsend()returned. The Copilot session now matches the echo to a pending steering message by that ID first, and falls back to matching by text only when the echo or the pending message has no ID yet. Without this, two identical steering messages could be mixed up while one of them was being withdrawn: the removed one showed as a turn and the delivered one stayed pending.Withdrawing a steering message that the runtime already took for the running turn returns
removed: falseand does not interrupt that turn. The SDK interrupts a turn only to withdraw the message that started it.This PR covers the first scenario in #336466, where a removed message is still sent. It does not cover the second scenario, where a message is sent while it is being edited. #336895 addresses that scenario by disabling editing of pending steering messages, so this PR uses "Refs" instead of "Fixes".
Before / after
No GIFs. Recording the real Copilot agent was not possible because the test GitHub sign-in kept being revoked, so the agent could not run a turn. The proof is the deterministic tests below. They drive the same code paths with the existing fake SDK session and fake providers.
How to test
Manual check, signed in, with the Copilot agent in the chat view or the Agents window:
Start-Sleep 40in the terminal, then say DONE."Withdrew steering message.Automated:
What the new tests check
mainAgentService: a user can remove a steering message mid-turn when the provider can withdraw itwithdrawSteering is not a function). The fake SDK then delivers the message, as the real runtime would.withdrawMessagewith the ID and prompt it was sent, and no turn starts for the message.Validation
main@dc811b77cd1(fix reverted, tests kept)scripts\test.bat ... --grepcommand abovescripts\test.bat --runGlob "vs/platform/agentHost/test/**/*.test.js"npm run typecheck-clientnpm run eslint -- <the 9 changed .ts files>The Agent Host test run unsets
COPILOT_HOME. One existing test (isolates peer runtimes, preserves local adoption identity and refuses revoked prepared work) compares the SDK storage path againstprocess.env, so it fails wheneverCOPILOT_HOMEis set, as it is inside a Copilot CLI session. That test does not involve steering.This PR and #342373, which runs late steering messages as the next turn, both touch
sendSteeringand the queue drain contribution. Whichever merges second needs a small rebase.Found with automated exploratory testing and code reading; fixed with deterministic unit tests. Draft until reviewed by a human.