mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 12:40:32 +00:00
It affects AIR clients with the `asyncTasks` capability.
## Problem
- A subagent can start a background Bash or Monitor and then end its
turn. The task ends after the subagent finished. The route of the task
dropped every update of a finished child generation, so the client never
got the terminal `async_task_state_update`. The task stayed running in
the client forever. One AIR log held 440 such dropped updates.
- The `Monitor` tool streams the stdout of a background command to the
model only. The SDK reports its task as `local_bash`, so the client
showed it as a background shell. In one real session, 18 of 18 Monitor
tasks had no output file, no progress and no result. The Stop button
only broke the agent's wait.
- The adapter read a task id from the text of a Bash tool result. A
foreground `sed`, `strings` or `python3` that printed adapter source,
test fixtures or CLI strings became a background task, for example with
the id `";\n const start = text` or `${e}`. The client got a spawn and a
`backgrounded` marker on a foreground call, and the task never ended.
- Claude Code 2.1.287 reports a background command in four texts, and
only one gave an output path. A subagent tool result almost never has
`tool_use_result` (0 of 48 in one session). So a subagent command that
hit its timeout got no output path (9 of 10).
## Fix
- `NativeSubagentRuntime.routeOfToolCall` still sends
`async_task_spawned`, `async_task_progress` and
`async_task_state_update` to the child generation that owned the tool
call after that child finished. It never sends them to a later
generation. Every other late update is dropped as before. The spawn is
in the list because a held task can get its tool call id after the
subagent finished.
- `AsyncTaskRuntime` takes a `toolNameOf` lookup. A task of a `Monitor`
tool call is ignored, like a subagent task: no spawn, no progress, no
state, and no stop. A held task that gets its tool call id later goes
through the same check. A `local_monitor` task is ignored too. The
`Monitor` tool call stays in the transcript.
- A task comes only from structured data:
`tool_use_result.backgroundTaskId` or the SDK task events. The text id
parser is gone.
- The output path of a known task can come from the result of its own
tool call. The path ends with `tasks/<id>.output` and starts after the
nearest `": "` on the same line, so a path with spaces works, for
example under `C:\Users\John Doe\AppData\Local\Temp`. It must be
absolute (POSIX, a Windows drive or UNC), and it never holds the end of
another path. The parse is linear in the text length. All four texts
give the path in both event orders: the tool result before or after the
SDK names the task. The memory is bounded. A structured path still wins,
and the terminal `output_file` reaches the client also after a level
close.
- The `backgrounded` marker comes only from structured data: the
structured task, a known background task of the tool call, or a
`run_in_background` input. The "both event orders" above holds for the
path, not for this marker. The marker rides the tool result update, so a
command that the SDK names only after its tool result gets the marker
only from a `run_in_background` input. A command that the user moved to
the background by hand gets no marker in that order.
- The replay behaves the same in each case.
## Tests
- Native subagent and agent tests: a task update and a late spawn after
the child finished go to `worker-1`; a held task of a subagent gets its
spawn and state in the child session after the subagent finished; a late
message chunk is still dropped; a subagent's background Bash ends after
the subagent and the client gets the terminal state in the child
session.
- Monitor: runtime, live and replay tests, also a task that names its
tool call after the tool result. A Monitor task cannot be stopped.
- Structured ids: foreground output with each marker text creates no
task; each of the four texts gives the path in both orders, also a
Windows and a POSIX path with spaces and a UNC path; no path of another
task, another tool call, `.outputs`, a relative path, a path without a
`": "` boundary, or a path across a line; two paths on one line are not
glued; a 2 MB text without `": "` parses in milliseconds; a structured
path wins. An agent test covers a subagent without `tool_use_result`,
and a replay test takes the path from the tool result.
- The new tests fail without the change. Build, lint, format and all
tests pass.
## Abandoned streamed tool calls
This part affects every client, Zed included.
**Problem.** The adapter opens a tool call at the `content_block_start`
of a streamed `tool_use`. Claude can abandon that block before it is
complete, for example for a steering message. The tool then never runs.
At end_turn the settle check reported the session failure "Claude ended
the turn without returning results for tool calls"
(`incomplete_tool_call`). A real AIR session showed a red service error
after 27 minutes of normal work, for a call that never had input.
**Fix.**
- The session records the tool calls that Claude Code sent to the tool
runner: the `tool_use` blocks of a complete assistant message, and the
tool calls of a permission request. The record is bounded and is dropped
when the call ends.
- At end_turn, an open foreground call without that record is abandoned.
The adapter closes it as failed with "Claude stopped this tool call
before it ran." and reports no session failure. The turn ends as
`end_turn`.
- An open call with that record keeps the old behaviour: the old failure
text and the `incomplete_tool_call` session failure.
- The cleanup and the cancellation checks are the same for both groups.
- The replay does not change, because the history holds no partial
`tool_use`. No scenario golden file changes, for AIR, plain or Zed.
**Tests.** A partial `tool_use` and then text ends as `end_turn` with
the neutral close. A mixed turn still fails for the unfinished call. A
steering message during a partial `tool_use` gives no failure. The
existing incomplete-tool tests now send the complete `tool_use` and keep
their failure. The new tests fail without the change.