Summary
When the model launches a background Bash(run_in_background) or an async
sub-agent during a turn, that work keeps running after end_turn. The client has
no reliable ACP signal for (a) the set of background tasks currently outstanding,
or (b) when each one finishes. The launch tool result returns immediately with a
backgroundTaskId, so the corresponding ACP tool_call reaches completed at
launch, not at process exit — and nothing later flips it.
Already in the SDK stream, but dropped
The adapter consumes, but does not forward, the signals that carry this:
background_tasks_changed is explicitly dropped
(case "background_tasks_changed": break;), with a comment describing it as
exactly the UI signal — "the full live background-task set on every membership
change".
task_updated { patch.status: completed | failed | killed } and
task_notification only prune the internal subagentParentToolUseIds
attribution map; a terminal status is never translated into a tool_call_update
for the client.
Impact
An ACP client that tries to reconstruct background-task state from the CLI's
on-disk JSONL transcript can't do it reliably:
- a finished background shell record carries only
backgroundTaskId +
interrupted — no exit code, no status;
- an async sub-agent frequently has only a launch record with no matching
<task-notification> (still running, or completed after the client stopped
tailing).
So a "N background tasks running" indicator built on the transcript gets stuck
long after the work finished. The authoritative signal exists in the SDK stream
but never reaches the client. This affects any ACP client, not just ours.
Proposal (additive, backward compatible)
- Forward
background_tasks_changed as the full live set (ids + kind +
status), e.g. on _meta / an ext-notification. Because it's a level signal, a
client just mirrors it — no counting, no settlement bookkeeping, no races.
- (Optional) Translate
task_updated { status } into a tool_call_update
(completed / failed) on the originating Task tool call, using the
subagentParentToolUseIds map you already maintain, so per-sub-agent
completion shows inline.
Both can be gated behind a _meta key / capability flag to keep the default wire
shape untouched.
Prior art
The SDK itself frames background_tasks_changed as a level signal "for surfaces";
VS Code delivers background-command completion with the real exit code. The data
is available — it just isn't forwarded.
Environment: @agentclientprotocol/claude-agent-acp@0.58.1 (behavior described
from src/acp-agent.ts; line numbers omitted since they drift). Companion issue:
#864 (session-level active/idle signal for post-turn activity). Happy to send a PR.
Summary
When the model launches a background
Bash(run_in_background)or an asyncsub-agent during a turn, that work keeps running after
end_turn. The client hasno reliable ACP signal for (a) the set of background tasks currently outstanding,
or (b) when each one finishes. The launch tool result returns immediately with a
backgroundTaskId, so the corresponding ACPtool_callreachescompletedatlaunch, not at process exit — and nothing later flips it.
Already in the SDK stream, but dropped
The adapter consumes, but does not forward, the signals that carry this:
background_tasks_changedis explicitly dropped(
case "background_tasks_changed": break;), with a comment describing it asexactly the UI signal — "the full live background-task set on every membership
change".
task_updated { patch.status: completed | failed | killed }andtask_notificationonly prune the internalsubagentParentToolUseIdsattribution map; a terminal status is never translated into a
tool_call_updatefor the client.
Impact
An ACP client that tries to reconstruct background-task state from the CLI's
on-disk JSONL transcript can't do it reliably:
backgroundTaskId+interrupted— no exit code, no status;<task-notification>(still running, or completed after the client stoppedtailing).
So a "N background tasks running" indicator built on the transcript gets stuck
long after the work finished. The authoritative signal exists in the SDK stream
but never reaches the client. This affects any ACP client, not just ours.
Proposal (additive, backward compatible)
background_tasks_changedas the full live set (ids + kind +status), e.g. on
_meta/ an ext-notification. Because it's a level signal, aclient just mirrors it — no counting, no settlement bookkeeping, no races.
task_updated { status }into atool_call_update(
completed/failed) on the originatingTasktool call, using thesubagentParentToolUseIdsmap you already maintain, so per-sub-agentcompletion shows inline.
Both can be gated behind a
_metakey / capability flag to keep the default wireshape untouched.
Prior art
The SDK itself frames
background_tasks_changedas a level signal "for surfaces";VS Code delivers background-command completion with the real exit code. The data
is available — it just isn't forwarded.
Environment:
@agentclientprotocol/claude-agent-acp@0.58.1(behavior describedfrom
src/acp-agent.ts; line numbers omitted since they drift). Companion issue:#864 (session-level active/idle signal for post-turn activity). Happy to send a PR.