Summary
We (the olai editor, an ACP client that ships this adapter pinned) needed Monitor and Bash(run_in_background) calls to stay open on the wire until the harness reports their end, and we have that working as a local patch on 0.66.0. It is #865's problem and #941's approach generalised, and before doing anything else with it we would like to know whether a PR of it would be welcome here.
The patch, applied by our nix build: https://github.com/juspay/olai/blob/chat-background-tasks/acp/patches/background-tasks-visible.patch — with its rationale, and a line-by-line record of what came from #941 and what did not, in acp/patches/README.md.
The gap it closes
A background task's launch acknowledgement is read as the call's result, so its ACP tool_call reaches completed at the moment the task starts. For a client that means an armed watch and a finished one are the same row: nothing to tick, no liveness, and — the one that motivated this — no death. A person supervising a dispatch off kolu watch … --nag 10m had nothing on screen saying whether the monitor was still up, or that it had exited.
#941 fixes exactly this for async Agent/Task, and says so: "background Bash retain their existing behavior". So Monitor and Bash(run_in_background) — the two tools a client most wants to draw a live face for — are untouched by it.
What the extension does
Taken from #941 (its approach is the basis, and we would not have got here without it): a launch acknowledgement may not settle the call; the provider's terminal states map onto ACP's two, with the provider's own word preserved; a tool's structured answer is trusted only behind its cached tool name; and the ordering races it enumerates (terminal before acknowledgement, permission-emitted cards, straggling results).
Added:
- coverage as a table rather than a branch —
Monitor ({ taskId, … }), Bash(run_in_background) ({ backgroundTaskId, … }), async Agent/Task ({ status: "async_launched", agentId }). A tool the table does not name keeps today's lifecycle, which is the safe direction to fail in;
- correlation and metadata off
task_started.tool_use_id, which also names task_type and the description the task was armed with — neither of which is in the structured answer, and both of which a client wants to draw ("kolu fleet watch" is what a person recognises their own watch by; the call's title is Bash);
_meta.claudeCode.backgroundTask on every frame about such a call — { taskId, taskType, description } when armed, { status, summary } when it settles;
- the settle carries the harness's own summary, which is where a background shell's exit code is (Background command "…" failed with exit code 3).
task_updated is the guaranteed half of the bookend and carries no summary, so it settles the call and the task_notification beside it refines the same call with the sentence — ACP's own upsert rule rather than a second mechanism;
background_tasks_changed used only to bound the retained record (a task that has both settled and left the live set is forgotten), never as a settle in itself: the level carries no status, and closing a call on an absence would invent the fact this exists to report.
Verified against real sessions with the vendored CLI: a Monitor (armed → in_progress at 5.1s, completed + summary at 11.5s, in no turn at all), a background Bash exiting 3 (failed + "…failed with exit code 3"), a foreground Bash and a synchronous subagent both unchanged.
One measurement that may be useful regardless
While checking what a client could draw per event, we recorded the full SDK stream (emitRawSDKMessages: true) for a Monitor whose command printed several lines. The stream carried task_started, background_tasks_changed at each end, task_updated and task_notification — and not one frame containing the monitor's own output. The lines reach the model (it answers about them) and the task's output_file; nothing in the stream carries them, so an adapter has nothing to forward per event and a client cannot draw one. That is a CLI-side gap rather than an adapter one, and we mention it only because it is easy to assume the adapter is dropping something here. Our driver that re-measures it: packages/tests/tasks.ts.
The ask
Would you welcome a PR of this — either on top of #941 once that lands, or as a change to it if the author prefers?
Two notes if the answer is yes. Our patch is written against the compiled dist/acp-agent.js, because npm is the channel we pin through; against src/acp-agent.ts it is a mechanical translation, and we would send it as source with tests in your own style. And we have not pushed a branch or opened a PR here — this issue is the whole of what we have done on this repo.
Related: #865 (the issue), #941 (the PR this builds on).
Summary
We (the olai editor, an ACP client that ships this adapter pinned) needed
MonitorandBash(run_in_background)calls to stay open on the wire until the harness reports their end, and we have that working as a local patch on 0.66.0. It is #865's problem and #941's approach generalised, and before doing anything else with it we would like to know whether a PR of it would be welcome here.The patch, applied by our nix build: https://github.com/juspay/olai/blob/chat-background-tasks/acp/patches/background-tasks-visible.patch — with its rationale, and a line-by-line record of what came from #941 and what did not, in
acp/patches/README.md.The gap it closes
A background task's launch acknowledgement is read as the call's result, so its ACP
tool_callreachescompletedat the moment the task starts. For a client that means an armed watch and a finished one are the same row: nothing to tick, no liveness, and — the one that motivated this — no death. A person supervising a dispatch offkolu watch … --nag 10mhad nothing on screen saying whether the monitor was still up, or that it had exited.#941 fixes exactly this for async
Agent/Task, and says so: "background Bash retain their existing behavior". SoMonitorandBash(run_in_background)— the two tools a client most wants to draw a live face for — are untouched by it.What the extension does
Taken from #941 (its approach is the basis, and we would not have got here without it): a launch acknowledgement may not settle the call; the provider's terminal states map onto ACP's two, with the provider's own word preserved; a tool's structured answer is trusted only behind its cached tool name; and the ordering races it enumerates (terminal before acknowledgement, permission-emitted cards, straggling results).
Added:
Monitor({ taskId, … }),Bash(run_in_background)({ backgroundTaskId, … }), asyncAgent/Task({ status: "async_launched", agentId }). A tool the table does not name keeps today's lifecycle, which is the safe direction to fail in;task_started.tool_use_id, which also namestask_typeand thedescriptionthe task was armed with — neither of which is in the structured answer, and both of which a client wants to draw ("kolu fleet watch" is what a person recognises their own watch by; the call's title isBash);_meta.claudeCode.backgroundTaskon every frame about such a call —{ taskId, taskType, description }when armed,{ status, summary }when it settles;task_updatedis the guaranteed half of the bookend and carries no summary, so it settles the call and thetask_notificationbeside it refines the same call with the sentence — ACP's own upsert rule rather than a second mechanism;background_tasks_changedused only to bound the retained record (a task that has both settled and left the live set is forgotten), never as a settle in itself: the level carries no status, and closing a call on an absence would invent the fact this exists to report.Verified against real sessions with the vendored CLI: a
Monitor(armed →in_progressat 5.1s,completed+ summary at 11.5s, in no turn at all), a backgroundBashexiting 3 (failed+ "…failed with exit code 3"), a foregroundBashand a synchronous subagent both unchanged.One measurement that may be useful regardless
While checking what a client could draw per event, we recorded the full SDK stream (
emitRawSDKMessages: true) for aMonitorwhose command printed several lines. The stream carriedtask_started,background_tasks_changedat each end,task_updatedandtask_notification— and not one frame containing the monitor's own output. The lines reach the model (it answers about them) and the task'soutput_file; nothing in the stream carries them, so an adapter has nothing to forward per event and a client cannot draw one. That is a CLI-side gap rather than an adapter one, and we mention it only because it is easy to assume the adapter is dropping something here. Our driver that re-measures it:packages/tests/tasks.ts.The ask
Would you welcome a PR of this — either on top of #941 once that lands, or as a change to it if the author prefers?
Two notes if the answer is yes. Our patch is written against the compiled
dist/acp-agent.js, because npm is the channel we pin through; againstsrc/acp-agent.tsit is a mechanical translation, and we would send it as source with tests in your own style. And we have not pushed a branch or opened a PR here — this issue is the whole of what we have done on this repo.Related: #865 (the issue), #941 (the PR this builds on).