session: client-execution entries no longer raise InputNeeded - #380
Merged
Conversation
SessionStatus.InputNeeded is documented as "blocked waiting for user input or tool confirmation". A toolClientExecution entry is neither: the call has already cleared its confirmation gate and is simply running on a client. Counting it meant a session reported "input needed" for the whole duration of every client tool call, and that a call kept presenting as blocked after the user had already approved it. withInputNeededStatus now promotes only when the queue holds a user-blocking entry, ported across the Go, Rust, Kotlin and Swift reducers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
connor4312
marked this pull request as ready for review
August 4, 2026 15:28
connor4312
enabled auto-merge
August 4, 2026 15:28
connor4312
added a commit
to microsoft/vscode
that referenced
this pull request
Aug 4, 2026
* agentHost: drive tool execution from the session input queue Subagent tool calls could stall indefinitely. A user reported 16 subagents running overnight that "keep stalling and dying for no apparent reason", needing the main agent to repeatedly repair them. Log analysis found 16 permission requests that were never answered, and 80 subagent chat channels unsubscribed ~12ms after a single provider error. The cause is structural rather than a single bug. Answering a tool call was owned by the per-turn chat observer: it rendered the call AND invoked the tool AND dispatched the outcome. So anything that tore down an observer -- a provider error disposing the parent turn's store, a turn ending, a reconnect, or simply never observing a subagent chat -- left the agent blocked on an obligation nobody was left to answer. Invert the relationship. The protocol already maintains SessionState.inputNeeded: a session-level queue of every outstanding blocker, each entry self-sufficient so a client can answer it without subscribing to the owning chat. It is a derived projection recomputed from tool-call status, so it is a set that can be re-read rather than a stream that can be missed. Make that queue the driver: - A session-level watcher owns all four blocker kinds and is the single caller of invokeTool. Chat observers only render. - One shared ChatToolInvocation per call, created by whichever side arrives first, so the card an observer renders in its subagent group is the same object the watcher executes. - Claimed calls run with chat context so confirmations render inline. Unclaimed non-confirmable calls run headlessly. Unclaimed confirmable calls wait for an observer, then deny rather than surface a modal nobody can see. - Chat input requests and MCP authentication get the same treatment; both could previously stall with no surface at all. This removes the class rather than the instances: an obligation is now answered because the session says it is outstanding, not because some particular observer happened to still be alive. Also stop counting toolClientExecution entries as user-blocking. That entry means a client is running the tool, not that a user was asked, so it must not raise InputNeeded -- otherwise every client tool call flags the session as needing input for its whole duration, and an approved call keeps presenting as blocked. Mirrors microsoft/agent-host-protocol#380. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * agentHost: share one input-needed watcher per backend session Sibling resources (default, peer and subagent chats) can be open against the same backend session at once, and each installed its own session-level watcher over the same inputNeeded queue. Each had independent per-request state, so one client-tool request executed the tool once per open resource; _resolveToolCall only deduplicates the eventual dispatch, long after the tool's side effects have already run N times. Ref-count a single watcher per backend session instead, keeping it alive while any sibling holds a reference. The resource-to-backend mapping is recorded at install time rather than resolved during teardown, when provisional session state may already be gone. The claim registry now records which observer is rendering a request, so a claimed tool executes with that observer's chat context instead of whichever sibling happened to install the watcher. Also reattach the withInputNeededStatus documentation, which described the old "any non-empty queue" rule and had come loose from its function. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.
Problem
SessionStatus.InputNeededis documented as "A turn is in progress but blocked waiting for user input or tool confirmation."withInputNeededStatuspromoted on any non-emptyinputNeededqueue, includingtoolClientExecutionentries. But a client execution is not a prompt — the call has already cleared its confirmation gate and is merely running on a client. Two consequences today:toolSearch, browser tools, …).225encodes exactly this — atoolConfirmationis replaced by atoolClientExecutionfor the same id, and the status stays24. The user approves, and the session still shows as blocked on them.Change
Promote only when the queue holds a user-blocking entry:
Ported to the four hand-maintained reducers (Go, Rust, Kotlin, Swift). Docs on
SessionState.inputNeededandSessionToolClientExecutionRequestupdated to state the exception; generated types/schemas regenerated vianpm run generate.The entry itself is unchanged — it stays in
inputNeededso a client can still discover and execute the work from the session channel alone. Only its effect onstatuschanges.Tests
225updated: swapping a confirmation for a client execution now drops toInProgress.261added: setting a client execution leaves an in-progress sessionInProgress.262added: removing the last user-blocking entry clearsInputNeededwhile a client execution remains.Validation
types/reducers.test.tstsc --noEmit+eslint types/verify:change-fragments/verify:changeloggo build+go test ./...cargo testswift buildAnyCodable.swift(CFGetTypeIDunavailable). No errors fromReducers.swift.Please let CI cover Swift and Kotlin.
Why now
Downstream in
microsoft/vscodethis blocks putting auto-approved client tool calls intoinputNeeded. They are currently excluded precisely to avoid this status flash, which leaves them with no session-level record — so if the owning turn's observer is torn down, the tool call stalls with no recovery path. That is the root cause of a reported incident where subagents repeatedly stalled overnight and had to be manually restarted.Once this lands, that exclusion can be dropped and every blocked tool call becomes discoverable and answerable from the session channel.