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.
ACP adapter for the Claude Agent SDK
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
/mcpin the chat: the MCP server status as a list. The adapter runs/mcp reconnect,/mcp enable, and/mcp disablethrough 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
_metakeys: 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).