You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Ask an agent to do two things that each need approval in a single turn — the canonical case is "read file A and file B" where the read tool is gated with an ask rule. The model issues both tool calls together in one turn, but the human is shown only one approval card. You answer it, the tool runs, and only then does the second card appear. You approve the files strictly one at a time, one card per turn, even though the agent asked for both at once.
This is the user-visible half of #5373 ("HITL breaks on multi-file approval flow… the interaction takes additional turns"). PR #5382 built the runner-side machinery to show and answer several cards together, but a live QA pass found the cards never arrive together, because the harness never asks for them together.
Why: the ACP adapter serializes permission requests
Live QA on the EE dev stack (driving the real product endpoint, asserting on the SSE frame stream and the runner log, never on model prose) established that both agent harnesses serialize their permission requests:
Claude (claude-agent-acp 0.58.1). Two parallel gated tool calls in one turn produce exactly onerequest_permission from the adapter. The adapter blocks on that request and does not issue the next one until it is answered. The runner logs its [HITL] ACP gate … line (which sits at the entry of the permission handler, before any pause or park logic) exactly once per turn, even when two tool-input-available frames for two distinct tool-call ids stream on the wire.
Pi (pi-builtin gate path). Same shape: two parallel gated bash calls in one turn, both tool inputs stream, but only one[HITL] pi-gate … line is logged and only one tool-approval-request frame is emitted. The sibling is force-settled.
In both cases the runner pauses the turn on the first gate; the sibling gated call is force-settled as TOOL_NOT_EXECUTED_PAUSED (surfaced as a tool-output-error frame) and re-raised on the next turn after the first gate is answered. On the warm keep-alive path this next gate arrives on the same live session with no model re-plan between them, so the chain is not slow — but it is still one card at a time, one turn per gate.
tool-input-available toolu_013f… (Read /etc/hostname)
tool-approval-request toolu_013f… approvalId=7defd9b6 <- the ONE card
tool-input-available toolu_01FU… (Read /etc/os-release) <- second read's input arrives after the card
tool-output-error toolu_01FU… <- second read force-settled, not carded
finish finishReason=other <- turn parks on the one gate
Where the block lives
The serialization is upstream of the runner. The runner's plural machinery (PR #5382: one card per gate keyed by tool-call id, a parkedApprovals map, one warm resume that answers each gate by its own permissionId) is ready to surface and answer several concurrent cards. It is never exercised live because the adapter only ever hands the runner one pending request_permission at a time. The adapter's blocking respondPermission contract — issue one permission request, await its answer, then continue the prompt — is what forces the one-at-a-time UX.
Direction (not yet scoped)
Confirm at the ACP layer whether claude-agent-acp (and the Pi builtin-gate path) can hold severalrequest_permission calls open concurrently, or whether serialization is baked into the adapter's prompt loop.
If it cannot be changed upstream, decide whether the runner should synthesize the concurrent set itself (raise cards for every announced-but-not-yet-gated sibling in the same turn), rather than force-settling siblings and waiting for the adapter to re-raise them.
What happens
Ask an agent to do two things that each need approval in a single turn — the canonical case is "read file A and file B" where the read tool is gated with an
askrule. The model issues both tool calls together in one turn, but the human is shown only one approval card. You answer it, the tool runs, and only then does the second card appear. You approve the files strictly one at a time, one card per turn, even though the agent asked for both at once.This is the user-visible half of #5373 ("HITL breaks on multi-file approval flow… the interaction takes additional turns"). PR #5382 built the runner-side machinery to show and answer several cards together, but a live QA pass found the cards never arrive together, because the harness never asks for them together.
Why: the ACP adapter serializes permission requests
Live QA on the EE dev stack (driving the real product endpoint, asserting on the SSE frame stream and the runner log, never on model prose) established that both agent harnesses serialize their permission requests:
claude-agent-acp0.58.1). Two parallel gated tool calls in one turn produce exactly onerequest_permissionfrom the adapter. The adapter blocks on that request and does not issue the next one until it is answered. The runner logs its[HITL] ACP gate …line (which sits at the entry of the permission handler, before any pause or park logic) exactly once per turn, even when twotool-input-availableframes for two distinct tool-call ids stream on the wire.pi-builtingate path). Same shape: two parallel gatedbashcalls in one turn, both tool inputs stream, but only one[HITL] pi-gate …line is logged and only onetool-approval-requestframe is emitted. The sibling is force-settled.In both cases the runner pauses the turn on the first gate; the sibling gated call is force-settled as
TOOL_NOT_EXECUTED_PAUSED(surfaced as atool-output-errorframe) and re-raised on the next turn after the first gate is answered. On the warm keep-alive path this next gate arrives on the same live session with no model re-plan between them, so the chain is not slow — but it is still one card at a time, one turn per gate.Turn-1 wire frames (Claude, "read /etc/hostname and /etc/os-release",
permission.default = ask)Where the block lives
The serialization is upstream of the runner. The runner's plural machinery (PR #5382: one card per gate keyed by tool-call id, a
parkedApprovalsmap, one warm resume that answers each gate by its ownpermissionId) is ready to surface and answer several concurrent cards. It is never exercised live because the adapter only ever hands the runner one pendingrequest_permissionat a time. The adapter's blockingrespondPermissioncontract — issue one permission request, await its answer, then continue the prompt — is what forces the one-at-a-time UX.Direction (not yet scoped)
claude-agent-acp(and the Pi builtin-gate path) can hold severalrequest_permissioncalls open concurrently, or whether serialization is baked into the adapter's prompt loop.Links
docs/design/agent-workflows/projects/concurrent-approvals/PLAN.md(open question 3 flagged exactly this risk)