Skip to content

Background-task lifecycle beyond #941: keeping Monitor and background Bash open on the wire (patch, olai) #1038

Description

@srid

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 branchMonitor ({ 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions