Summary
claude-agent-acp resolves the session/prompt request on the SDK result
message (mapped to a stopReason such as end_turn). This is deliberate — it
unlocks the client for the next prompt without waiting for the trailing idle. But
the Claude session is frequently not idle at that moment: after a background
sub-agent's task-notification the model resumes and keeps producing, and
autonomous turns can fire later. The adapter forwards that activity as
session/update notifications on the same session, after the turn's stopReason
was already returned, with no session-level signal telling the client the
session is still working.
Result: a client shows "idle/done" the instant end_turn lands, even though more
output is still streaming in; and a client that follows the ACP spec (updates must
be sent before the session/prompt response) is entitled to drop those
post-settlement updates and silently lose the output.
Root cause
ACP has a single signal — the pending session/prompt RPC — that conflates two
things a Claude session needs to keep separate:
- "the turn is settled" → the user can send the next prompt;
- "the session is idle" → nothing is being produced anymore.
For responsiveness the adapter settles the turn early (on result), so from the
client's side these two collapse into one, and the "still working" phase after
end_turn becomes invisible / unframed.
Already in the SDK stream, but not forwarded
session_state_changed (idle/active) is consumed only to backstop turn
settlement — the code comment notes idle carries no turn identity, and it can lag — and its active/idle state is never surfaced to the client (the only
client-facing effect on that branch is a title check). So the signal that would
let a client bracket the post-turn activity exists, but doesn't reach the wire.
Proposal (additive, backward compatible)
Keep the current result-based settlement (don't regress responsiveness), and
also forward session_state_changed as a session-level active/idle
notification distinct from the turn stopReason — e.g. on _meta / an
ext-notification. That lets a client:
- keep a "still working" indicator until the session is genuinely idle;
- treat the span between
end_turn and the next idle as a well-defined
out-of-turn episode, so post-settlement session/updates can be rendered
instead of dropped.
Optionally, tag out-of-turn session/updates with an origin marker (the adapter
already distinguishes task-notification-origin messages internally) so clients
can label them without inferring from timing.
Prior art
Zed's own client models "busy" as "the session/prompt RPC is still pending";
VS Code surfaces post-turn background completion via a system-initiated
notification. Both rely on a signal that survives the turn boundary — which is
exactly what's missing here.
Environment: @agentclientprotocol/claude-agent-acp@0.58.1 (behavior described
from src/acp-agent.ts; line numbers omitted since they drift). This is one of
two related issues; the companion (reliable background-task tracking) is linked in
a comment. Happy to send a PR if you're open to the direction.
Summary
claude-agent-acpresolves thesession/promptrequest on the SDKresultmessage (mapped to a
stopReasonsuch asend_turn). This is deliberate — itunlocks the client for the next prompt without waiting for the trailing idle. But
the Claude session is frequently not idle at that moment: after a background
sub-agent's
task-notificationthe model resumes and keeps producing, andautonomous turns can fire later. The adapter forwards that activity as
session/updatenotifications on the same session, after the turn'sstopReasonwas already returned, with no session-level signal telling the client the
session is still working.
Result: a client shows "idle/done" the instant
end_turnlands, even though moreoutput is still streaming in; and a client that follows the ACP spec (updates must
be sent before the
session/promptresponse) is entitled to drop thosepost-settlement updates and silently lose the output.
Root cause
ACP has a single signal — the pending
session/promptRPC — that conflates twothings a Claude session needs to keep separate:
For responsiveness the adapter settles the turn early (on
result), so from theclient's side these two collapse into one, and the "still working" phase after
end_turnbecomes invisible / unframed.Already in the SDK stream, but not forwarded
session_state_changed(idle/active) is consumed only to backstop turnsettlement — the code comment notes
idle carries no turn identity, and it can lag— and its active/idle state is never surfaced to the client (the onlyclient-facing effect on that branch is a title check). So the signal that would
let a client bracket the post-turn activity exists, but doesn't reach the wire.
Proposal (additive, backward compatible)
Keep the current
result-based settlement (don't regress responsiveness), andalso forward
session_state_changedas a session-level active/idlenotification distinct from the turn
stopReason— e.g. on_meta/ anext-notification. That lets a client:
end_turnand the nextidleas a well-definedout-of-turn episode, so post-settlement
session/updates can be renderedinstead of dropped.
Optionally, tag out-of-turn
session/updates with an origin marker (the adapteralready distinguishes
task-notification-origin messages internally) so clientscan label them without inferring from timing.
Prior art
Zed's own client models "busy" as "the
session/promptRPC is still pending";VS Code surfaces post-turn background completion via a system-initiated
notification. Both rely on a signal that survives the turn boundary — which is
exactly what's missing here.
Environment:
@agentclientprotocol/claude-agent-acp@0.58.1(behavior describedfrom
src/acp-agent.ts; line numbers omitted since they drift). This is one oftwo related issues; the companion (reliable background-task tracking) is linked in
a comment. Happy to send a PR if you're open to the direction.