Skip to content

session: client-execution entries no longer raise InputNeeded - #380

Merged
connor4312 merged 1 commit into
mainfrom
input-needed-excludes-client-execution
Aug 4, 2026
Merged

session: client-execution entries no longer raise InputNeeded#380
connor4312 merged 1 commit into
mainfrom
input-needed-excludes-client-execution

Conversation

@connor4312

Copy link
Copy Markdown
Member

Problem

SessionStatus.InputNeeded is documented as "A turn is in progress but blocked waiting for user input or tool confirmation."

withInputNeededStatus promoted on any non-empty inputNeeded queue, including toolClientExecution entries. 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:

  • A session reports Input Needed for the entire duration of every client tool call (toolSearch, browser tools, …).
  • Worse, approving a call does not clear it: fixture 225 encodes exactly this — a toolConfirmation is replaced by a toolClientExecution for the same id, and the status stays 24. The user approves, and the session still shows as blocked on them.

Change

Promote only when the queue holds a user-blocking entry:

function awaitsUser(request: SessionInputRequest): boolean {
  return request.kind !== SessionInputRequestKind.ToolClientExecution;
}

function withInputNeededStatus(status, inputNeeded) {
  if (inputNeeded.some(awaitsUser)) { /* … InputNeeded */ }
  /* … clear the input-needed bit */
}

Ported to the four hand-maintained reducers (Go, Rust, Kotlin, Swift). Docs on SessionState.inputNeeded and SessionToolClientExecutionRequest updated to state the exception; generated types/schemas regenerated via npm run generate.

The entry itself is unchanged — it stays in inputNeeded so a client can still discover and execute the work from the session channel alone. Only its effect on status changes.

Tests

  • 225 updated: swapping a confirmation for a client execution now drops to InProgress.
  • 261 added: setting a client execution leaves an in-progress session InProgress.
  • 262 added: removing the last user-blocking entry clears InputNeeded while a client execution remains.

Validation

Suite Result
types/reducers.test.ts 255 passing
tsc --noEmit + eslint types/ clean
verify:change-fragments / verify:changelog pass
Go go build + go test ./... pass
Rust cargo test pass
Swift swift build ⚠️ not verified — fails on this Windows box in the untouched AnyCodable.swift (CFGetTypeID unavailable). No errors from Reducers.swift.
Kotlin ⚠️ not verified — no Gradle locally.

Please let CI cover Swift and Kotlin.

Why now

Downstream in microsoft/vscode this blocks putting auto-approved client tool calls into inputNeeded. 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.

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
connor4312 marked this pull request as ready for review August 4, 2026 15:28
@connor4312
connor4312 enabled auto-merge August 4, 2026 15:28
anthonykim1
anthonykim1 approved these changes Aug 4, 2026
@connor4312
connor4312 merged commit ae3973f into main Aug 4, 2026
9 checks passed
@connor4312
connor4312 deleted the input-needed-excludes-client-execution branch August 4, 2026 15:36
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants