Ben Brandt b007ac7c45 fix: end a cancelled turn at its interrupted result and deliver Claude Code's follow-up after a cancel (#1309)
After a cancel, Claude Code ends the interrupted turn with an aborted
result, then keeps working through its queue, and reports `idle` once at
the end. The queue can hold a prompt it had already taken off it, then
its own follow-ups, such as the summary of a background command that
finished. The adapter settled a cancelled turn only at that final
`idle`, and dropped everything in between until the next prompt
activated. As a result:

- **v1:** a cancelled `session/prompt` waited for any follow-up queued
behind it to finish.
- **v2:** a follow-up after a cancel streamed as text with no `running`
or `idle`.

This PR ends the cancel where Claude Code's own messages say the stopped
work ends.

## What changes

- **Where a cancelled turn settles.** It now settles at the result of
the work the interrupt stopped: its own result, or, for a turn held open
for its subagents, its follow-up's aborted result.
- The `idle`, the next echo's hand-off and the force-cancel backstop
still settle a turn whose result never comes.
- That result owes its trailing `idle` like any settling result, a
steered turn's included.
- **When the cancel ends.** The cancel records the prompts it stopped:
the unsettled turns and their steers when it arrives, and whatever it
abandons when it sweeps the queue.
- With `request_marker_lists_v1`, every `status: requesting` frame lists
the prompts its model request serves. A cycle Claude Code starts on its
own lists none.
- The cancel ends at the first request that serves none of the recorded
prompts, once no turn is unsettled and no orphan result is outstanding.
Compaction updates resume there too.
- A prompt Claude Code had already dequeued names itself, so its answer
stays dropped even when the interrupt receipt leaves it out.
- Without the capability, the cancel ends at the next activation, as
before.
- **Abandoned prompts' results.** A result that names a cancelled prompt
never promotes the next queued prompt, so a prompt sent after the cancel
can't take an abandoned prompt's result.
- **Held turns.** Since CLI 2.1.270, Claude Code reports `running` for
as long as background agents live. So with request markers, a held
turn's follow-up counts as live only when a model request has started
since the turn's result. A cancel while only the subagent runs settles
the held turn at once.
- **v2: aborted cycles.** A cycle whose result is aborted
(`terminal_reason` `aborted_streaming` or `aborted_tools`) ends
`cancelled`.

## Evidence

**What CLI 2.1.293 sends.** I recorded the stream live with a probe that
drives the SDK the way the adapter does. After an interrupt:
1. The interrupted turn's aborted result comes first.
2. Next, any prompt Claude Code had already taken off its queue runs.
Its request names it.
3. Then its own follow-ups run. Their requests name no prompt.
4. Finally, one `idle`.

The interrupt stops background agents, but not background commands.

**An audit of the bundled CLI** found exactly one source of `status:
requesting` frames: one per main query-loop iteration. Subagents,
compaction, side queries, hooks and retries emit none, or only a named
one.

**The adapter, end to end over ACP against the live CLI**, this branch
against `main`:

| Scenario | `main` | This PR |
| --- | --- | --- |
| v1: a background command finishes while the turn the user cancels
streams | `session/prompt` answered 14.1 s after the cancel | answered 4
ms after the cancel; the follow-up's text arrives after |
| v2: same | follow-up text with no `running`; `idle cancelled` 14.8 s
after the cancel | `idle cancelled` 10 ms after the cancel, then
`running`, the follow-up, `idle end_turn` |
| v2: a background command finishes after the cancelled turn | `idle
cancelled`, then text with no `running` or `idle` | `idle cancelled`,
then `running`, the follow-up, `idle end_turn` |
| v2: cancel while idle, then a background command finishes | text with
no `running` or `idle` | `running`, the follow-up, `idle end_turn` |
| v1: cancel while a background subagent holds the prompt open | (not
compared) | answered 2 ms after the cancel |

## Tests

- **20 new tests, scripted on the recorded orders.**
- v1 suite "output after a cancel" (12): a cancelled turn and a cancel
while idle; a dequeued prompt the receipt leaves out; a steer; a prompt
that Claude Code folds into its own follow-up; a prompt sent while the
cancel is still in progress; a repeated cancel; trailing idles after a
cancelled turn and after a steered one; older CLIs without request
markers.
- Held turns (3): a cancelled held turn settles at its follow-up's
aborted result, not at a placeholder result, and at once when only its
subagent runs.
- v2 (5): the follow-up after a cancelled turn as its own cycle; a
cancel while idle; a later cycle after a cancelled one; a cycle Claude
Code started before the cancel arrived; a follow-up's compaction.
- **Mutation checks.** Removing each rule in turn fails at least one
test. Two exceptions:
  - Clearing the record at activation can't be observed.
- Holding for any unsettled turn, not only a recorded one, can't be told
apart from activation.
- `npm run check`, build and the full suite pass, with no golden file
changes.

## Review

The design went through two rounds of independent review.

- **The first attempt, rejected.** It judged "the stopped work is over"
from the adapter's own bookkeeping. Review found that the interrupt
stops what Claude Code is running, which the adapter may not have seen
yet. That attempt is not in this PR.
- **This design.** A second round audited the CLI's signals and the
state machine. This PR fixes what it found:
- a prompt sent after a cancel could take an abandoned prompt's result;
  - a held turn could settle at a placeholder result;
- the window could close while an abandoned prompt that Claude Code
folds into its own follow-up was still due;
  - a prompt sent during the cancel's awaits wasn't recorded;
  - a held turn's follow-up was misread as live on CLI 2.1.270+.

## Known limitations

- **The cancelled turn's last streamed text still arrives after the
cancel**, as on `main`: the cancel never held streamed text back.
- **If the cancel lands just after Claude Code started its own
follow-up**, the interrupt stops that follow-up, and its partial output
gets through. On v2 it ends `cancelled`.
- **Trailing-idle debt can be over-counted by one in a drain run.** The
next `running` transition sweeps it, as before.
2026-10-12 19:17:16 +02:00
2026-07-21 18:53:56 +02:00
2025-09-02 19:06:32 -03:00
2026-03-26 12:19:06 +01:00

ACP adapter for the Claude Agent SDK

npm

Use Claude Agent SDK from ACP-compatible clients!

This tool implements an ACP agent by using the official Claude Agent SDK, supporting:

  • Context @-mentions
  • Images
  • Tool calls (with permission requests)
  • Compact file changes through the negotiated AIR diff patch extension
  • Following
  • Edit review
  • TODO lists
  • Nested subagent transcripts
  • Interactive (and background) terminals
  • Custom Slash commands
  • Client MCP servers
  • /mcp in the chat: the MCP server status as a list. The adapter runs /mcp reconnect, /mcp enable, and /mcp disable through the SDK control API, because Claude Code refuses them in SDK mode. A reconnect of an ACP server that needs authentication starts MCP OAuth through URL elicitation
  • Session-scoped long-running goals for AIR through the goal extension under _meta.jetbrains.air.goal
  • Structured errors, recovery, and warnings through the opt-in session failure extension
  • Concrete model and effort defaults through the opt-in recommended config value extension
  • Tool permission presentation, editable choices, and durable effects through the permission extension
  • One fact per field in every tool call report, as the ACP tool call contract defines
  • All AIR extensions, capabilities, and _meta keys: AIR extensions

Learn more about the Agent Client Protocol.

To try changes that have landed on main but are not released yet, install from the preview channel — every push to main publishes one. See docs/RELEASES.md.

npm install @agentclientprotocol/claude-agent-acp@preview

Subagent sessions

Subagents are exposed only after bilateral capability negotiation. Until the released ACP SDKs preserve the draft clientCapabilities.subagents field, a supporting client may advertise nativeSubagentSessions in _meta.jetbrains.air.capabilities; the adapter mirrors the capability in its initialize response. The canonical field remains supported and takes precedence once it is available. Without either client signal, Agent/Task lifecycle keeps its legacy ordinary ACP tool-call representation and child interactions stay on the root session. Clients that use the historical _meta["subagent-transcript"] capability or forwardSubagentText session option retain the flattened child transcript behavior. See AIR extensions.

Contribution Policy

This project does not require a Contributor License Agreement (CLA). Instead, contributions are accepted under the following terms:

By contributing to this project, you agree that your contributions will be licensed under the Apache License, Version 2.0. You affirm that you have the legal right to submit your work, that you are not including code you do not have rights to, and that you understand contributions are made without requiring a Contributor License Agreement (CLA).

Description
Pull mirror of agentclientprotocol/claude-agent-acp (Apache-2.0): the ACP bridge that lets Buzz drive Claude Code
Readme 107 MiB
Languages
TypeScript 99.2%
JavaScript 0.6%
Shell 0.2%