Repository navigation
feat: sync upstream v0.93.0 and Claude Agent SDK 0.3.302 - #20
Merged
Merged
Conversation
…ol#1287) Bumps `@agentclientprotocol/sdk` from 1.7.0 to 1.8.0. The release is one change: the protocol schema moves to v1.25.0 and v2.0.0-alpha.8. ## What changed in the SDK Comparing the generated types of 1.7.0 and 1.8.0, the only type change in v1 and v2 is the `error` stop reason from [agent-client-protocol#2281](agentclientprotocol/agent-client-protocol#2281): `IdleStateUpdate` gains `ErrorStopReason & { stopReason: "error" }`, and `ErrorStopReason` (an optional JSON-RPC `error`) is new. The v2 index no longer exports `StopReason`; the adapter doesn't import it. ## The one code change `idle()` in `src/v2/prompt.ts` built the v2 `state_update` for an ended turn by spreading the turn's stop reason into a `SessionUpdate`. With the larger `IdleStateUpdate` union, TypeScript can no longer match a union of stop reasons against it (it falls through to the custom and unknown members). The state is now typed as the members for the agent's own stop reasons (`Extract<IdleStateUpdate, { stopReason: StopReason }>`). A stop reason that v2 doesn't know is still a compile error, which I checked with a throwaway probe. The output is unchanged. ## Not in this PR The adapter doesn't use the new `error` stop reason yet. Two stand-ins stay until a follow-up switches them, which changes behavior and golden files: - a failed v2 turn's `_error` stop reason with `_meta.claudeCode.error`, and its error `notice` (`src/v2/prompt.ts`); - a failed subagent's `_meta.claudeCode.error` in the RFD form (agentclientprotocol#1257, not merged yet). ## Checks - `npm run check`, `npm run build`, and all 2489 tests pass. - The scenario suites validate every message against the schemas that 1.8.0 ships (`schema/schema.json`, `schema/v2/schema.unstable.json`), loaded from the installed SDK. No golden file changes.
…tal ACP v2 surface (agentclientprotocol#1288) Follow-up to agentclientprotocol#1287 (SDK 1.8.0). A v2 turn that fails after its prompt was taken in now ends with the standard `error` stop reason from [agent-client-protocol#2281](agentclientprotocol/agent-client-protocol#2281), in place of the stand-ins we used while the SDK didn't have it. ## Before and after Before, on the v2 surface only: 1. an error `notice`, titled with the failure; 2. `idle` with the custom stop reason `_error`, and the JSON-RPC error in `_meta.claudeCode.error`. Now, one update: ```json { "sessionUpdate": "state_update", "state": "idle", "stopReason": "error", "error": { "code": -32603, "message": "Internal error: API Error: 529 Overloaded" } } ``` - `error` is the JSON-RPC error that v1 rejects the prompt with: same code, message and data. A client can show `message` and handle `code` as for an error response, so `-32000` (auth required) can start its sign-in again, which `_error` couldn't. - The notice is gone. The v2 draft's [Failures](https://agentclientprotocol.com/protocol/v2/draft/prompt-lifecycle) section reports a failure that ends the work through the stop reason, and keeps notices for failures the agent recovers from. - v1 and AIR are unchanged. ## Also - `TurnEvents.failed` loses its `title` parameter. Only the v2 notice read it, and v1's prompt already ignored it. Its pass-through in `Turn.reject` and `failActive` goes too. AIR's session failures still get the title, through their own path. - The v2 scenario harness treats an `error` stop reason as a failed turn, and reports its message. ## One visible difference The old notice showed the failure title, for example `API Error: 529 Overloaded`. The error's message is v1's, `Internal error: API Error: 529 Overloaded`, which v1 clients already show. If we'd rather v2 clients see the shorter text, the alternative is to use the title as the error's `message` and keep v1's code and data. That would make v2's message differ from v1's. ## Checks - `npm run check`, `npm run build` and all 2489 tests pass. No golden file changes: no recorded scenario fails a turn after insertion. - The two v2 failure tests pin the new update and the auth code. Putting `_error` back fails both.
… ACP v2 surface (agentclientprotocol#1289) v2 has a `cancelled` tool call status ("the tool call was cancelled before it completed"), which v1 doesn't. Until now a v2 client got `failed` for a tool call the user interrupted, the same as a real failure. ## What changes Claude Code stamps a tool result that carries no tool output with `tool_result_meta.non_execution_kind`. For a **v2 client**: | Kind | Meaning (set in the bundled CLI) | Status | | --- | --- | --- | | `interrupted` | the turn was interrupted while the tool ran | `cancelled` | | `cancelled` | the call's own run was stopped (aborted, or moved to the background) | `cancelled` | | `user-rejected`, `permission-rule`, `automode-*` | the tool was refused | `failed`, as before | v1 and AIR clients are unchanged: `failed`, with the kind in `_meta.claudeCode.nonExecutionKind`. v2 clients still get that `_meta` too. ## How - **The renderer decides the status** (`AcpToolCallRenderer.resultStatus`), from `ClientCapabilities.v2`. That's the same pattern the agent uses for v2 diffs: the value travels as a v1 status (`V2_CANCELLED`, with a cast, since v1's `ToolCallStatus` is closed), and the v2 surface passes it through. The v2 translation still doesn't read `_meta`. The rule covers the regular result and the two fallback reports in `toAcpNotifications`: an untracked tool call, and a TodoWrite/Task tool call that only a permission request surfaced. - **The agent's own "finished" checks** now use `isFinalToolCallStatus`, which counts `cancelled`. Two places compared against `completed`/`failed`: one removes a finished call from the turn's foreground work, the other drops its eager-session and dispatched-call entries. Without this, a cancelled call would have leaked those entries. ## Not covered Replay doesn't have `tool_result_meta`, so a replayed interrupted call stays `failed`, just as v1 replay has no `nonExecutionKind`. ## Checks - `npm run check`, `npm run build` and all 2493 tests pass. No golden file changes: no recorded scenario has an interrupted tool. - New tests: - **renderer:** each kind, for v2, v1 and AIR clients, plus the permission-surfaced fallback; - **agent:** a cancelled call is released from the dispatched-call bookkeeping; - **v2 surface:** a v2 client receives `cancelled`. - I removed each part of the change in turn, and each removal fails a test: never cancelled, cancelled for v1 too, refusals cancelled too, the bookkeeping ignoring `cancelled`, and the fallback branches keeping `failed`.
…erimental ACP v2 surface (agentclientprotocol#1290) The v2 draft lets the `session/new`, `session/resume`, and `session/fork` responses carry `availableCommands` ([agent-client-protocol#2259](agentclientprotocol/agent-client-protocol#2259)), so a client can show a session's commands without waiting for an update. The v2 surface now fills it. ## Before and after, for a v2 client - **Before:** the setup response, then an `available_commands_update` with the list, from a `setTimeout` after the response. - **Now:** the list is in the response itself, for `session/new`, `session/resume` with and without replay, and `session/fork`. The field is left out when the list is empty, as the draft asks. - **No duplicate:** on a v2 connection the agent no longer sends that post-setup update (`afterSetupResponse`). Later changes still arrive as `available_commands_update`, for example the terminal-bound filtering after Claude Code's first init frame, or `commands_changed`. - v1 and AIR are unchanged. ## Cost None. The commands come from `Query.supportedCommands()`, which in the SDK is `this.latestCommands ?? (await this.initialization).commands`: the cached initialization result that setup already awaited. So the response doesn't wait any longer. ## Code - `ClaudeAcpAgent.availableCommands(sessionId)` returns the session's command list, factored out of `sendAvailableCommandsUpdate`. - The three post-setup `setTimeout` blocks become one `afterSetupResponse`, which skips the command update on v2. - `setupCommands` in `src/v2/agent.ts` adds the list in v2 form. A failure to read the commands is logged and leaves them out, since they're optional and the session already exists. ## Title `refactor:`, like the earlier v2 surface additions (agentclientprotocol#1253, agentclientprotocol#1255, agentclientprotocol#1256), so no release note announces v2 yet. Retitle it if you'd rather. ## Checks - `npm run check`, `npm run build` and all 2494 tests pass. - The 40 v2 golden files each move the list from the post-setup notification into the setup response, and nothing else changes; I checked every diff with a script. No v1 or AIR golden file changes. - New v2 tests cover each setup response (new, resume, resume with replay, fork) and check that no post-setup update follows. - I removed each part in turn, and each removal fails tests: the agent still sending the update on v2, and the commands missing from new, from resume, or from fork. - Not covered: the empty-list case, because the adapter always adds its `/mcp` command.
…tclientprotocol#1268) ## Summary Adds the opt-in AIR `sessionIndex` extension. With it, an AIR client can show the latest sessions of a project quickly, with their state, and can rename, archive and unarchive them over ACP. The full contract is in `docs/air-extensions.md` ("Session index"). The extension follows ACP RFDs. The transport differs: it uses `_meta.jetbrains.air` request options and row fields, `_session/*` methods, and AIR capabilities that are not advertised under ACP v2. The `archived` list filter also differs (see below). - **Session list extensions** (agentclientprotocol/agent-client-protocol#2323): - `limit`, ordered by last user activity (`lastPromptAt ?? updatedAt`), and `includeWorktrees`. - Row fields `lastPromptAt`, `model` and `forkedFrom`. - `state`: `running`, `idle`, `requires_action`, and `error`, which the RFD lacks. - `lastTurnEndedAt`. - `cost`: the SDK's `total_cost_usd` for a session that runs on this connection only. No cost is read from transcripts. - Live updates come from an AIR list subscription (below). - **Session archive** (agentclientprotocol/agent-client-protocol#2161): - `archived` is on every row, `session_info_update` is sent on a change, and an unknown session is `-32002`. - The `archived` list filter has three values: `"unarchived"` (the default), `"archived"` (archived sessions only) and `"all"` (both, in one order). #2161 has a boolean here. Its `false` is `"unarchived"`, its `true` is `"all"`, and `"archived"` has no counterpart. - **Capabilities**: an AIR client that declares `sessionIndex` gets these in `_meta.jetbrains.air.capabilities`. The client declares only `sessionIndex`. - `sessionIndex`. - `sessionArchive`: `_session/archive` and `_session/unarchive`. The RFD capability is `archive` (#2161). - `sessionRename`: `_session/rename`. The RFD has capability `setTitle` and method `session/set_title` (agentclientprotocol/agent-client-protocol#1987). - `sessionListSubscribe`: `_session/list/subscribe`, `_session/list/unsubscribe` and `_session/list/changes`. No RFD counterpart. ### Clients without `sessionIndex` Clients that don't declare `sessionIndex`, which means non-AIR clients and released AIR builds, keep the current behaviour. They get the same SDK `listSessions` path, page size, cursor and initialize capabilities, with no watchers and no notifications. There are two deliberate deviations, both strictly safer: 1. **`session/delete` from an AIR client without `sessionIndex`.** Released AIR builds use delete as "Done". Instead of deleting the transcript (data loss), the adapter archives the session in AIR's own Claude format (below). Archived sessions are hidden from that client's list, so "Done" looks the same and nothing is lost. An unknown session keeps the SDK's error. Other clients get the SDK delete, as before. 2. **`session/close`** of a session that is not loaded here returns `{}` instead of throwing. ### Sessions open in another process `session/load`, `session/resume` and `session/prompt` work on a session that another live Claude Code process has open, for every client, as the Claude CLI does. Rename, archive, unarchive and delete also go ahead, writing records or deleting transcripts as the CLI's `/rename` and delete do. The other process keeps running. ### Archive format and rule The archive state uses AIR's native Claude archive format (`ClaudeStoreThreadBackend.appendTitleRecords`, ADR 0140), so AIR's native path and the adapter read and write the same state. - **Archive** appends `{"type":"custom-title","customTitle":"[archived] <title>",…}` and `{"type":"agent-name","agentName":"[archived] <title>",…}` to every transcript copy. **Unarchive** appends the same pair without the prefix. Titles are cut to the CLI's 200 characters. - **Archived is decided by the agent name first**, as in AIR's native Claude provider. A session's name is its last top-level `agentName`, else the custom title the SDK reports. The session is archived exactly when that name starts with `[archived] `. The listed title comes from the same source, shown without the prefix. - Title records are metadata: `updatedAt` and the list order do not move. ### Rename, archive and delete - **Rename** writes the same title pair. An archived session keeps its prefix. A session running here is renamed through its CLI's `rename_session` control request, serialised with auto-titling. Other sessions get the records plus the `custom-title.json` sidecar the CLI writes. No generated title replaces a client title, but a title stored later by someone else (a `/rename`) is still adopted. - **Archiving stops the running session.** `_session/archive` of a session loaded on this connection does four things: 1. It interrupts the running turn. 2. It retitles the session through its CLI. The adapter also appends the matching `agent-name` record to the CLI's transcript, because the CLI writes that record only later. 3. It closes the session as `session/close` does. 4. It archives the transcripts once more if the closed CLI left them unarchived. The client then gets `session_info_update` with `archived: true`. Unarchive does not reopen the session. A session that is not loaded here is archived by writing the title records. - **Delete** for a `sessionIndex` client deletes every copy of the session and its `<sessionId>/` directories. A session running here is closed first. ### Implementation (`src/session-index/`) - **Index**: - Reads the project dirs with `readdir` + `stat`, and keeps a 2000-entry metadata LRU keyed by (path, mtime, size). - Cache misses are read in parallel through `getSessionInfo`, plus a growing tail read (64 KB → 4 MB) for the last prompt, last message, turn end and agent name. - SDK semantics are kept: sidecar titles, `continued-in`, long-path hashes, darwin case, and first-prompt extraction (ported, and fuzz-compared with the SDK). - The `cwd` scope follows the SDK `listSessions`. Worktrees expand to the same relative subdirectory, and a session copied to several project directories is listed from its newest copy. - The keyset cursor is bound to `cwd`, `archived` and `includeWorktrees`. Session ids match exactly. - **State**: - For sessions of this process, the SDK state. - For other sessions, the Claude Code live registry (`~/.claude/sessions/<pid>.json`, with pid + procStart liveness and Claude Desktop's rules) or the transcript tail. The registry is read only for the row state. - `error` means the session is not running and its last turn ended with an error, not a cancel. Documented in `docs/air-extensions.md`, with an extension → RFD mapping table and the remaining differences. ### Measured on a real `~/.claude` (233 transcripts, read-only) | Case | New path | Old SDK path | |---|---|---| | cwd, limit 50, cold process | ~80 ms | 78–156 ms (all rows) | | cwd, limit 50, warm | 1.3–4 ms | — | ## Session list subscription Replaces polling the first page. It is only for a client that declared `sessionIndex`; the agent advertises `sessionListSubscribe` with it, never under ACP v2. - **Subscribe**: `_session/list/subscribe { cwd }` returns `{ subscriptionId }`. `cwd` must be absolute, else `-32602`; other params are ignored. - The scope is `cwd` and the same subdirectory in every linked worktree, in any archive state, without sidechains. - **Changes**: `_session/list/changes { subscriptionId, sessions, removed }`. - `sessions` holds the full `session/list` row of each session that appeared, or whose title, `lastPromptAt`, `state`, `lastTurnEndedAt`, `cost`, `model`, `forkedFrom` or `archived` changed since the last row sent. - The first change of a session after subscribe is sent in full. After that, a change of `updatedAt` alone sends nothing. - `removed` lists sessions that were deleted or moved out of scope. - Archive and unarchive are row changes. - A session is sent at most once a second, with one notification per subscription per pass. - **Unsubscribe**: `_session/list/unsubscribe { subscriptionId }` returns `{}` and is idempotent. - A subscription lives until unsubscribe or until the connection closes. - A connection has at most 128 subscriptions. The 129th is `-32602` with `data.reason: "too_many_subscriptions"`. - Subscriptions of the same `cwd` share one watch and are notified independently. - **Delivery** is best effort, with no resync and no sequence numbers. A rescan every 10 s (and 1 s after a project directory watch opens) sends whatever differs. - **Implementation** (`list-subscriptions.ts`, `session-info.ts`): - A non-recursive `fs.watch` on the scope's project dirs, the registry and the projects root. - A transcript event re-reads only that transcript, and a registry event reads only that `<pid>.json`. Sessions running here push their SDK state at once. - Subscribe reads no session row. The rescan reads only transcripts whose stat changed or whose read failed. ### Measured: one subscription on the 5,200-session project (Docker `node:22`, 2 CPU, 2 GB, shared host) 300 s of churn: 20 sessions appending 10/s each (updatedAt-only), registry status flips every 200 ms, and a session created every 2 s and deleted 10 s later. | | Old `list_changed` watcher | Subscription | |---|---|---| | Agent CPU p50 / max | 19% / — | 12.9% / 49% | | Event loop p99 (p50 / max of 1 s windows) | — | 16 / 31 ms; worst single delay 87 ms | | Change latency p50 / p95 | 950 ms (hint) | create 291 / 570 ms, delete 279 / 506 ms; registry state 902 / 990 ms* | | Notifications / min | — | 90 (896 rows, 0 rows for updatedAt-only appends) | | RSS | flat ~160 MB | 159 → 198 MB (heap flat ~50 MB) | \* The registry flips the same 5 sessions every 200 ms, so each is held to the once-a-second limit. Latency is measured from the first unsent change. These numbers were measured with an earlier subscribe that read every row up front. Subscribe now reads no rows, so that one-off cost no longer occurs. The lazy subscription has not been re-measured yet. ## Load test: 10,000 sessions (Docker, 2 CPU, 2 GB) Calibrated data: 10k transcripts, 15 GB, 36k subagent files, and one 5,200-session project. Sizes match a real profile (p50 311K / p90 2.67M / p99 20.5M). | Scenario (limit 50) | Before | After | |---|---|---| | Big project, first page cold / warm p50 | out of memory | 225 / 19 ms | | No cwd, cold / warm p50 | 1,722 / 178 ms | 209 / 45 ms | | Page through 4,637 rows | 457 s | 11.4 s cold, 2.9 s warm | | Delete p50 / 100 concurrent | 23.8 ms / 4.4 s | 7.8 ms / 0.8 s | | 300 s of churn (60k changes) | out of memory after 3 s | list p50 54 ms, 0 stalls >50 ms, RSS flat ~160 MB | ## Streaming: keep the event loop responsive under a backlog Under a backlog of buffered SDK messages, the adapter could freeze its event loop for seconds. Every `query.next()` on a non-empty SDK queue resolves in a microtask, so `runConsumer` forwarded the whole backlog without reaching a macrotask boundary. Incoming JSON-RPC, including `session/cancel`, waited until the backlog drained. 1. **Consumer yields to the event loop.** A shared `EventLoopYielder` (`src/utils.ts`) gives the event loop a turn via `setImmediate` once the current iteration has run for 8 ms. Order, content and cancel semantics are unchanged. 2. **No per-message copy in `nodeToWebWritable`** for byte streams. Object-mode streams keep the copy. 3. **Diff memo.** The approval preview and the PostToolUse patch no longer diff the same texts twice. Timed-out diffs are not remembered. 4. **Prefix guards** in `parseMcpCommand` and `sanitizeTitle`. Both give identical output. 5. **Replay yields between history messages**, with the same budget. With 10 sessions × 1 MB, the max event-loop delay went from 4205 ms to 1626 ms, and ping p95 from 2793 ms to 262 ms (interleaved A/B, Docker `node:22`, 2 CPU). Single-stream throughput is unchanged. Remaining stalls are inside the Claude SDK and GC. ## Test plan - [x] Rebased on `origin/main`. `npm run check`, `npm run build` and `npm run test:run` pass. - [x] Regression tests for the unchanged non-`sessionIndex` paths. - [x] Regression tests for every review finding. - [x] Reviews: Codex (gpt-6-astra) and a Claude review subagent, run as independent passes against `origin/main` until a round found nothing of normal or higher severity. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
🤖 I have created a release *beep* *boop* --- ## [0.89.0](agentclientprotocol/claude-agent-acp@v0.88.0...v0.89.0) (2026-10-09) ### Features * add the AIR sessionIndex extension for fast session lists ([agentclientprotocol#1268](agentclientprotocol#1268)) ([03d84f6](agentclientprotocol@03d84f6)) * **deps:** Bump @agentclientprotocol/sdk to 1.8.0 ([agentclientprotocol#1287](agentclientprotocol#1287)) ([446a721](agentclientprotocol@446a721)) * Support custom instructions for agent sessions via meta ([agentclientprotocol#1177](agentclientprotocol#1177)) ([908c994](agentclientprotocol@908c994)) ### Bug Fixes * recreate live sessions on logout when it clears a provider or gateway override ([agentclientprotocol#1278](agentclientprotocol#1278)) ([966be7a](agentclientprotocol@966be7a)) * report a failed turn with the error stop reason on the experimental ACP v2 surface ([agentclientprotocol#1288](agentclientprotocol#1288)) ([060bdb2](agentclientprotocol@060bdb2)) * report an interrupted tool call as cancelled on the experimental ACP v2 surface ([agentclientprotocol#1289](agentclientprotocol#1289)) ([55c3106](agentclientprotocol@55c3106)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: acp-release-bot[bot] <246668977+acp-release-bot[bot]@users.noreply.github.com>
…across pages (agentclientprotocol#1293) Two findings of the 2026-10-10 night perf run on a 5,000-session home (findings 8 and 9). ## Rename push (finding 9) The run measured "no push within 10 s" after `_session/rename`. The cause was the harness, not the adapter. The bench renames the newest session to `"Night renamed"`, and an earlier bench run on the same home had already given it that title. The transcript shows that pair of title records twice. A rename to the current title appends nothing, so there was nothing to push. On a fresh home, the same bench gets the push. The hypotheses checked: - The metadata cache is not updated by a rename. The appended records change the transcript's size, and the watcher re-reads it. - The sidecar lies outside the non-recursive watch, but so does every rename. Each one also appends to the transcript, which is watched. Still, the push depended on `fs.watch` plus its 150 ms quiet period. Now a rename, archive, unarchive or delete through this connection publishes at once. `ListSubscriptions.sessionWritten` stats the session's transcripts and reads its row, the way a transcript event would, without waiting. The event that follows finds the row unchanged and sends nothing. Renames by other processes still arrive through the watcher. The 1 s per-session interval of a subscription still applies. ## Listing every page (finding 8) Profiling `SessionIndex.list` showed where a page's time goes. The full enumeration and stat of 5,000 transcripts takes about 20 ms per page. Most of the remaining ~380 ms was the SDK `getSessionInfo`, called once per row read: its lookup runs a `readdir` of the whole project directory (5,000 entries) on every call. - A metadata read no longer calls the SDK. Titles come from the head and tail the index already reads, by the same rule (`sdkTitles`). The tail is searched as the SDK searches it, cut first line included. - The pages after a cursor reuse the list's enumeration. It is keyed by the cursor and kept for at most 30 s, at most 8 lists. It is dropped as soon as a directory of the scope adds or removes an entry, through the `DirListings` identity check. Such a page stats only the transcripts it reads (each with its session's other copies) right before reading them. A transcript that changed since either keeps its order key (no new prompt) or moves up before the cursor, so the page is as fresh as one from a new enumeration. The first page always enumerates. ## Measurements Real adapter (`dist/index.js`), driven as AIR drives it by `acp-perf-harness/air-ui/night/acp-adapter-bench.mjs`. Fixture: 5,000 sessions from `gen-night-data.mjs`. | | before | after | |---|---|---| | Listing every page (25 × 200) | 10.1–16.3 s | 0.92–1.16 s | | First page (51) | 156–472 ms | 46–62 ms | | Show more (50) | 117–283 ms | 13–23 ms | | Push after archive | ~170 ms | 3–4 ms | | Push after a rename | ~170 ms | 3–5 ms | ## Tests - A rename, archive, unarchive and delete each push within 500 ms while the watcher's quiet period is 60 s (fails without the fix). A rename by another process is pushed through the watcher. - Paging: - one enumeration for all pages; - between pages, a transcript emptied, one with a new prompt, and one with only a new answer give the same pages as a new enumeration; - an older copy of a session that got a prompt hides it on later pages; - adding or removing a transcript enumerates again. - A custom title whose record starts before the tail window matches the SDK. - Read-count assertions use the new `SessionIndex.transcriptsRead` counter instead of SDK call counts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
🤖 I have created a release *beep* *boop* --- ## [0.89.1](agentclientprotocol/claude-agent-acp@v0.89.0...v0.89.1) (2026-10-10) ### Bug Fixes * push renamed sessions to list subscribers and reuse one listing across pages ([agentclientprotocol#1293](agentclientprotocol#1293)) ([92abb8f](agentclientprotocol@92abb8f)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: acp-release-bot[bot] <246668977+acp-release-bot[bot]@users.noreply.github.com>
… experimental ACP v2 surface (agentclientprotocol#1291) A v2 diff can't hold the snippet of an Edit's input (it needs line numbers and the operation), so on v2 an auto-approved Edit or Write showed its change only once the PostToolUse hook reported it, after the tool had already run. Only an asked one showed its approval patch first. ## What changes, v2 only The tool call shows the exact patch before the tool runs, auto-approved or not. ## How - **A PreToolUse hook for Edit and Write** (`fileChangePreviewHook`), registered only on v2 connections. Claude Code runs the tool only after the hook returns, so the file is still unchanged when the hook reads it. That's the same guarantee the approval patch in `canUseTool` relies on. The hook sends the same patch (`previewPatchContent`): - **Stream already showed the tool call:** a `tool_call_update` through the session's stream router, pinned like an approval patch. - **Hook came first:** the SDK dispatches hook callbacks as soon as it reads them, ahead of the message queue the agent consumes. The hook then emits the tool call with the patch via `ensureToolCallEmitted`, the permission flow's fix for the same race (agentclientprotocol#851). - **Subagent tools** are attributed exactly as in `canUseTool`. That rule moves into `toolUseOwner`, which both use, with no behavior change. - **Safe failure:** the hook makes no permission decision, and any failure only leaves the preview out. - **v1 and AIR** register no PreToolUse hook, so Claude Code waits on nothing new there and their output is unchanged. I first tried hanging the preview on the per-tool-use hook registry that PostToolUse uses. That fails when the hook comes before the stream, so the hook lives on the agent instead. ## Golden files The scenario harness now runs PreToolUse before each tool, as Claude Code does, and applies each hook's `matcher`. The v2 golden files of the four file-change scenarios change: - **`write-existing`** (auto-approved): the patch now arrives before the tool completes, and the final hook report no longer repeats it, because the tracker sees no change. - **`write-new`, `edit-with-permission`, `edit-rejected`:** the tool call gets the patch just before the permission request, which carries the same patch. No v1, AIR or plain golden file changes. ## Checks - `npm run check`, `npm run build` and all 2497 tests pass. - A new v2 test runs an auto-approved Edit with the hook both after and before the streamed tool use. It checks that the patch is computed from the file before the edit, arrives before the result, and isn't replaced. - A new test checks that only a v2 session registers the hook, with the `Edit|Write` matcher. - Mutations that fail tests: no hook on v2, the hook on v1 too, the hook-first path doing nothing, the already-shown path sending nothing, and Edit only. - Not pinned by any test: the pin itself. On v2 nothing later in the stream carries content for an Edit or Write, so it's only there for consistency with the approval patch. ## Follow-up AIR has the same gap for an auto-approved Write: it shows no diff until the hook. That's out of scope here, since it changes AIR's output.
…celled on the experimental ACP v2 surface (agentclientprotocol#1300) v2 has a `cancelled` tool call status ("the tool call was cancelled before it completed"), which v1 doesn't. agentclientprotocol#1289 started using it for a run that Claude Code aborted. This PR uses it for the other tool call that never ran: one that Claude dropped before running it. ## What changes When a turn ends with `end_turn` and a streamed `tool_use` never reached a complete assistant message, Claude dropped that tool call before it ran, for example when it switched to a steering message. The agent closes it at the end of the turn with "Claude stopped this tool call before it ran." The turn itself still succeeds. | Case | v2 client | v1 and AIR clients | | --- | --- | --- | | Dropped before it ran | `cancelled` (was `failed`) | `failed`, as before | | Ran, but Claude Code returned no result | `failed`, as before, and the turn fails with `incomplete_tool_call` | same | The content text is unchanged in both cases. ## How - **The renderer builds both closing reports** (`AcpToolCallRenderer.unfinished`), and picks the status from `ClientCapabilities.v2`, as `resultStatus` does since agentclientprotocol#1289. Before, the agent built the report inline with a hardcoded `failed`. - **The agent** passes whether the tool ran. Its closing step (`failToolCall`, now `closeUnfinished`) already removes the call from all of its tracking itself, so no "is this status final" check needed changing. ## Checks - `npm run check`, `npm run build` and all 2728 tests pass. No golden file changes: no recorded scenario drops a tool call. - New tests: - **renderer:** dropped and ran-without-result, on v2, v1 and AIR; - **v2 surface:** a v2 client receives `pending` then `cancelled` for a dropped tool call, and the turn ends `end_turn`. - The existing v1 tests in `incomplete-tools.test.ts` are unchanged and still pass. - I made five mutations and each failed a test: the renderer never cancels, the renderer cancels a tool that ran, the agent marks a dropped call as having run, the agent marks a no-result call as dropped, and the agent's renderer ignores the client's capabilities.
…he experimental ACP v2 surface (agentclientprotocol#1301) agentclientprotocol#1289 made a v2 client get `cancelled` for a run that Claude Code aborted, when Claude Code reports it as an error result with `non_execution_kind`. Claude Code has a second way to report an aborted command: a **successful** Bash result whose structured output says `interrupted: true`. A v2 client got `completed` for that. With this PR it gets `cancelled` too, so the rule is the same however Claude Code reports the abort. ## When Claude Code sends that result I read the CLI that SDK 0.3.293 bundles. I didn't reproduce these cases live. | What stops the command | What the adapter receives | v2 before | v2 after | | --- | --- | --- | --- | | The client cancels (`session/cancel`) | an error result, `non_execution_kind: "interrupted"` | `cancelled` (agentclientprotocol#1289) | same | | A timeout | an error, "Command timed out after …", **not** `interrupted` (or the command is backgrounded) | `failed` | same | | The output limit or a replaced output file | an error with `Exit code 137`, not `interrupted` | `failed` | same | | A `now`-priority message such as `_session/steering`, landing just before the command starts, or while the output limit kills it | a successful result, `interrupted: true` | `completed` | **`cancelled`** | A successful `interrupted` result needs the abort reason `interrupt`, which a `now`-priority message causes. That abort doesn't kill a running command, so this result only comes from the races in the last row. Clients will rarely see it. The point of the change is that v2's status follows what Claude Code reports, however it reports it. v1 and AIR clients are unchanged: `completed` with no exit code, and AIR still gets exit code 1. ## How - `AcpToolCallRenderer.resultStatus` takes the command's `interrupted` fact alongside `nonExecution`. - `result()` used to call `resultFields`, which drops the command facts. It now computes the facts once (`resultFacts`) and builds the fields from them (`fieldsOfResult`). That lets it pass `interrupted` without adding a field to `RenderedResult`, which the public `toolUpdateFromToolResult` returns. - The agent's checks for a finished tool call already count `cancelled` (`isFinalToolCallStatus`, agentclientprotocol#1289). ## Checks - `npm run check`, `npm run build` and all 2730 tests pass. No golden file changes: no recorded scenario has an interrupted command. - New tests: - **renderer:** an interrupted command on v2, v1 and AIR, and one that isn't interrupted on v2; - **v2 surface:** a v2 client receives `cancelled`, and the turn still ends `end_turn`. - I made four mutations and each failed a test: the rule ignores `interrupted`, every client gets `cancelled`, `result()` doesn't pass the fact, and `result()` inverts it.
…clientprotocol#1265) IDEs can pass their own Claude hooks through `_meta.claudeCode.options.settings.hooks` on `session/new` and `session/load`. Before this change, any session settings replaced `CLAUDE_MODEL_CONFIG` entirely, so adding a single hook dropped the deployment's `modelOverrides` / `availableModels`. The two are now merged, and the session settings win per key. The wrapper does not merge hooks itself: Claude Code already runs the hooks of every settings tier, so user and project hooks keep running next to the session ones. Hooks stay scoped to their session, and a `session/load` with different settings recreates the query, which replaces or removes them. Co-authored-by: Claude Opus 5.5 <[email protected]>
…answer on the experimental ACP v2 surface (agentclientprotocol#1304) The first of two PRs that replace agentclientprotocol#1303, after a design review. This one restructures how the agent reports v2's state, with two small changes in behavior (below). The next PR makes v2 report `idle` at a turn's result and adds model cycles that Claude Code starts on its own. ## Why The agent reported each turn's v2 states (`running`, `requires_action`, `idle`) through the prompt's `TurnEvents`, the same events that answer the prompt. That ties v2's state to when **v1** gets its answer. The clearest case is the hold (`Turn.deferredSettle`): v1 holds a turn open while the background subagents it spawned run, because a v1 client stops listening at the prompt response. So v2 stays `running` too. Making v2 report `idle` at the turn's result in that design would need a v2 branch inside the hold logic. That's the most intricate code in the adapter: steering, cancel, the echo hand-off, ExitPlanMode, idle debts. And a cycle that Claude Code starts on its own would need a second source of v2 state beside the turn's (agentclientprotocol#1303). ## What changes - **`TurnEvents` only answer the prompt:** `inserted`, `ended`, `failed`. v1 answers at `ended` or `failed`. v2 answers at `inserted`, and uses `ended`/`failed` only to reject a prompt before insertion. - **New `ForegroundEvents`** (`src/turn-events.ts`): the session's foreground work, meaning what a client would wait for before sending the next prompt. Events: `running`, `awaitingUser`, `ended`, `failed`. - Only the v2 surface supplies them, through the new `foreground` option. They map to `state_update` in `v2Foreground`. - A prompt's turn is the foreground work from its insertion until it ends, exactly as before. So `idle` still waits for a held turn's subagents; the next PR changes that. - **The work is keyed by the session id the client knows,** not stored on a `Session`. A clear-context restart (an accepted ExitPlanMode with a fresh context) replaces the Claude Code session behind that id and moves the running turn to it. Work stored on the old `Session` would stay behind. - **Requests report through the work** (`syncAwaitingUser`), not through the active turn. ## Observable changes on v2 1. **The `session/prompt` response now comes before `running`, not after it.** - The 38 v2 golden files change by exactly that, and nothing else; I checked every file with a script. - The old order was not guaranteed: the SDK's `notify` writes after `waitUntilInitialized(...).then(...)`, and the response is written once the handler's promise resolves. The old code won that race by sending `running` before resolving. - The draft lets clients receive the updates before or after the response. 2. **A request that Claude Code opens for a prompt it started, before the prompt's echo, now reports `requires_action` when it opens,** rather than at the echo. The prompt's work is already running then. v1 and AIR clients are unchanged. ## Checks - `npm run check`, `npm run build` and all 2739 tests pass. - Every v2 state test in `acp-v2.test.ts` passes unchanged. - `turn-events.test.ts` now reads the waiting events from the foreground. - New tests: - a request opened before the echo; - the work staying with the session id across a clear-context restart, with a permission request in the continuation. - I made six mutations and each failed a test: no `running` at insertion, no end on `resolve`, no end on `reject`, waiting never reported, waiting only for the active turn, and the work tied to the `Session` object.
…cycles on the experimental ACP v2 surface (agentclientprotocol#1305) Stacked on agentclientprotocol#1304, which this targets; it retargets to `main` once agentclientprotocol#1304 merges. Together they replace agentclientprotocol#1303. ## What a v2 client gets now **`idle` at a turn's result,** also when v1 holds the turn's answer for the background subagents it spawned (`Turn.deferredSettle`). - The subagents' output and permission requests arrive while the session is idle. The draft allows that: background activity may send updates while idle, and they don't change the state. - A background request while idle no longer shows `requires_action`, which the draft reserves for foreground work. - The hold itself is unchanged. It now only delays v1's answer, with no v2 branch in the hold logic. **Model cycles that Claude Code starts on its own** are foreground work: the follow-up after a background subagent or command finished, or its answer to a message from another session. - A cycle reports `running` before its first output, `requires_action` while it waits, then `idle` with its own stop reason, usage and `_meta.quota`. - An error result gives `idle` with the `error` stop reason, and a cancel gives `idle cancelled`. - Before this, such a cycle streamed while v2 said `idle`. **Take-overs:** a prompt that Claude Code takes in during a cycle takes it over, with no new `running` and no `idle` in between. So do a steer that resumes a held turn, and the continuation of an accepted clear-context plan. In both, the final `idle` carries the turn's usage, not just the last cycle's. v1 and AIR clients are unchanged. ## How All of this builds on agentclientprotocol#1304's `ForegroundEvents`: - **A turn's work ends at its result:** `endForeground` in the defer branch of `settleOrDefer`, and where the steer idle lane re-applies a hold. - **A cycle starts** (`startAutonomousCycle`) at model output of the main conversation while no work runs. - Subagent output doesn't count. - Without command lifecycles, output while a prompt is queued stays that prompt's, since its output can come before its echo. With lifecycles (`msg_lifecycle_v1`), a prompt Claude Code hasn't started has no output yet, so a cycle starts. This narrows the gap that agentclientprotocol#1303 left. - **A cycle ends:** - at a result no turn took, except a placeholder result (`num_turns: 0`); - with `cancelled` at the diagnostic of an ExitPlanMode answer, as a turn does, unless a held turn continues the accepted plan; - at an SDK `idle` that no earlier result owes; - when the stream ends or fails; - before `session/close` answers. - **Starting work while a request is open** reports `requires_action` at once. - **The cycle's stop reason** comes from its result, as for a turn (`autonomousCycleEnd`). Its `_meta.quota` uses the same builder as a turn's (`quotaMeta`, extracted from `turnQuotaMeta`). ## Checks - `npm run check`, `npm run build` and all 2756 tests pass. No golden file changes against agentclientprotocol#1304: no recorded scenario has a held turn or output while no turn is active. - New tests: - **14 v2 tests:** agentclientprotocol#1303's eleven, plus a held turn reporting `idle` at its result with its follow-up as a cycle and a background request while idle, output before a queued prompt that Claude Code reports it hasn't started, and `session/close` during a cycle; - **agent:** a steer resuming a held turn's work; the follow-up that ExitPlanMode's "keep planning" interrupts ending `cancelled`; the clear-context continuation taking the cycle over, with the turn's usage; - **turn events:** work that starts while a request is open waits at once. - I made 20 mutations, one per rule above, and each failed a test. The take-overs for a steer and for the clear-context restart first survived, since a cycle stood in for the turn with the same states. The tests now check the usage of the final `idle` too. ## Not covered - No recorded scenario has such a cycle. The start signal (the first main-thread output) is chosen from the agent's side, not confirmed against a live CLI. - After a cancel, the agent drops main-thread output until the next prompt (`session.cancelled` stays set). So a follow-up that Claude Code runs after a cancel is still lost, for v1 and v2. That predates this PR, and I'll fix it separately.
…e Code's follow-up after a cancel (agentclientprotocol#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.
…eground on the experimental ACP v2 surface (agentclientprotocol#1310) v2's foreground work reported `requires_action` whenever any permission request or question in the session was open, including a background subagent's. The draft keeps `requires_action` for foreground work that is blocked on user action, and a background subagent asks without blocking the main conversation. So a v2 client saw a running turn as waiting on the user while it kept working. ## What changes A request blocks the foreground work when it is: - the main conversation's own request, or - the request of a subagent that runs in the foreground of the main conversation, so the tool call that spawned it waits on it. This applies up a chain of such subagents. A request from a subagent in the background, or from one nested inside it, no longer counts. - **Where each task runs.** The adapter records this per task: - from `task_started.is_backgrounded` and `parent_task_id`; - from a later `task_updated` patch that moves the task to the background; - Claude Code always resumes a subagent in the background. - **Unknown subagents.** A subagent without a record still counts, because its request can arrive before its `task_started`. Registering a task, or moving one to the background, checks the open requests again. - **Requests that name no agent** still count: MCP elicitations and dialogs. - **Steering is unchanged.** It still treats any open request as blocking. `Session.pendingUserInputCount` becomes `Session.openUserInputs`: the open requests, each with the subagent that asked. - **v1 is unchanged.** ## Evidence - **Live, CLI 2.1.293 via the SDK with a logging `canUseTool`:** - `task_started` carries `is_backgrounded: true` for a background subagent and `false` for a foreground one. - A subagent's `canUseTool` call carries `agentID` equal to its `task_started.task_id`. - `task_started` arrived before the subagent's request in both cases. - `parent_task_id` for nested subagents was not checked live; it's documented in the SDK types and covered by a test. - **6 new v2 tests:** - a background subagent's permission request leaves the turn `running`, and so does its question; - a foreground subagent's request reports `requires_action`; - a foreground subagent launched by a background one doesn't block; - a request that arrives before its subagent's `task_started` stops blocking once the subagent turns out to run in the background; - a request stops blocking once Claude Code moves its subagent to the background. - **Mutation checks.** Removing each rule in turn fails at least one test. That includes passing the subagent through for permission requests and for questions, the recheck at registration and at `task_updated`, and the walk up the chain. - `npm run check`, build and the full suite pass, with no golden file changes. Two resume tests now expect the resumed task to be recorded as backgrounded.
…ction on the experimental ACP v2 surface (agentclientprotocol#1311) A follow-up to agentclientprotocol#1310, from its review, which found no regressions but two wrong answers. Both come from one choice in agentclientprotocol#1310: a request from an agent without a `liveBackgroundTasks` record put v2's foreground work in `requires_action`. **When an agent has no record:** - **A teammate:** it never gets one. - **A settled subagent:** settling deletes its record, and settling didn't re-check open requests. - **A foreground subagent nested in a background one:** Claude Code leaves it out of `background_tasks_changed`, so the level reconciliation prunes it after two activations. The main conversation waits on none of them. A background subagent's request that the consumer read before its `task_started` also flickered from `requires_action` to `running`. ## What changes - **An agent without a record no longer blocks the foreground work.** A subagent the main conversation waits on has a record by the time it asks: its `task_started` comes first on the wire, and nothing prunes it while the main turn waits. When the consumer reads the request first, the `task_started` checks again, so `requires_action` comes slightly later instead of a flicker for background subagents. - **Settling a task re-checks** the open requests. - **The comment on `nativeSubagentParentByToolUseId`** no longer claims `parent_task_id` is unread. ## Tests - **Replaced:** "stops waiting for a request once its subagent turns out to run in the background". The new test checks that a request read before its `task_started` doesn't block, then blocks once the `task_started` says foreground. - **New:** a foreground subagent's open request stops blocking once Claude Code stops that subagent. - **Mutations:** restoring "no record blocks" fails both tests; dropping the re-check at settle fails the second. - `npm run check` and the full suite pass, with no golden file changes.
…ts task ends (agentclientprotocol#1312) A Bash call that sends its command to the background returns as soon as the command detaches, and its tool result carried `terminal_exit`. So a client that renders terminals, whether Zed's terminal extension or a v2 display terminal, showed the command's terminal as exited, with no code, at launch, while the command ran on. ## What changes For a client that renders terminals and isn't AIR: - **At launch:** the tool result of a command in the background leaves the exit out (`withRunningTerminal`). The tool call still completes; the v2 draft keeps "Terminal exit and tool call status are independent". - **When the command's task ends:** the consumer sends `terminal_exit`. It does so at the task's `task_notification`, or at its terminal `task_updated` if that comes first. The update is routed like the tool call, so a subagent's command ends in the subagent's session. - **No exit code.** Claude Code names the code only in the notification's summary text ("…failed with exit code 3"), which the adapter doesn't parse. `completed` doesn't mean 0 either: Claude Code accepts some non-zero codes as a success, such as grep's 1. The terminal still exits at the tool result, as before, in four cases: - **A fast command** whose task ended before its tool result. - **A task that started after the tool result.** Claude Code sends `task_started` first, but the existing `bash-background` scenario scripts the reverse, so its goldens don't change. - **AIR,** which shows the command through its async tasks. - **Replay,** where the command from before the reload no longer runs, and the structured tool result isn't available anyway. ## Evidence - **Live, CLI 2.1.293, through the built adapter over ACP:** - **v2:** the terminal stays running when the Bash call completes and the turn goes idle. `terminal_update` with `exitStatus: {}` arrives when `sleep 5` finishes, before Claude Code's follow-up. - **v1 with `terminal_output`:** no exit at the Bash result; `terminal_exit` with code `null` arrives when the command finishes. - **Recorded, CLI 2.1.293:** - the order is `task_started` (`local_bash`, `is_backgrounded: true`), then the Bash result with `tool_use_result.backgroundTaskId`, then `task_updated` and `task_notification` when the command ends; - neither event carries a structured exit code. - **Tests:** - v2: the terminal runs until the task ends, with one exit after the turn's `idle`; and a task that ended before the result exits at the result. - v1 with Zed's terminal extension: the exit comes after the prompt's answer. AIR keeps its exit at the result. - Unit tests for `withRunningTerminal`, including dropping a `_meta` that would be left empty, since an empty `_meta` would clear what a v2 client stores. - **Mutations:** removing the strip, the end-of-task exit, the liveness guard, or the AIR guard each fails a test. - `npm run check`, build and the full suite pass, with no golden file changes.
…rotocol#1313) ## Problem After a cancel, the consumer drops the main conversation's consolidated `assistant`/`user` messages until Claude Code starts a model request that serves none of the cancelled prompts (agentclientprotocol#1309). `stream_event` frames skipped that check. Claude Code always streams, so when it had already taken an abandoned prompt off its queue, the prompt's answer still streamed to the client as deltas. Only the consolidated copy was dropped. That output arrives after the cancel has been reported: on v1 after the turn's `cancelled` response, and on v2 after `idle cancelled`, outside any cycle. Both protocol versions require an agent's updates to come before that report. Streamed tool calls leaked the same way. agentclientprotocol#1309's tests missed this because they scripted only consolidated messages. ## Change - **After the client has been told the stopped work ended**, while the window is still open, streamed text, thinking and tool calls are dropped like the consolidated messages they belong to. On v1 that report is the turn's prompt response (no active turn). On v2 it's the idle state (no open foreground work). The v2 signal also covers cycles that Claude Code starts on its own and turns that v1 holds for their subagents. - **Before that report**, what the interrupt flushes still streams, as it does today. Both drafts allow updates between the cancel and the cancelled report, and the text is in Claude Code's transcript, so a reload shows it anyway. Existing tests pin this: v2's queued-prompt cancel, a held turn cancelled mid-follow-up, and a cancel that arrives during a buffered backlog. - **A prompt that is merely queued no longer keeps the window open.** agentclientprotocol#1309 held the window while any queued prompt was unsettled. So a follow-up that Claude Code ran ahead of a prompt sent after the cancel lost its consolidated copy, and with this change it would have lost its streamed copy too. Now a request holds the window only when: - the stopped turn is still unsettled; - an abandoned prompt's result is still expected; - the request names a cancelled prompt; or - the request names an unsettled prompt. That covers one that arrived while the cancel was still awaiting, before the cancel records it. - **Per-block drop latch.** Where the consolidated message delivers the text, a stream that loses a delta keeps dropping until its streamed-text record resets: at the next message, or when a consolidated message consumes the record. If a window closes mid-block (an older CLI's late prompt echo), the record holds nothing, so the consolidated message delivers the whole block once. Without the latch the record would hold only the tail, which isn't a prefix of the text, and the client would get the tail and then the whole text again. A client that gets subagent text only streamed has no consolidated copy to fall back on, so its subagent streams don't latch. - Bookkeeping for dropped events (message id, usage, compaction heartbeat) is unchanged. ## Evidence **Tests** - The "output after a cancel" suite now streams every answer the way Claude Code does, with Claude Code's follow-up in two chunks. Without the fix, 9 of its tests leak the streamed copy. - New v1 tests: - a follow-up that runs ahead of a prompt sent after the cancel; - a prompt sent during the cancel whose request arrives before the cancel abandons it; - an echo in the middle of a two-block answer; - a subagent stream for a client that gets subagent text only streamed. - New v2 tests: - an abandoned prompt's streamed answer; - what the interrupt flushes of a cycle that Claude Code started; - a held turn's follow-up that the cancel stopped before it showed anything. - **Mutation checks:** each of these fails at least one test: - removing any of the window's four rules, or restoring agentclientprotocol#1309's hold for any queued prompt; - removing the drop; - dropping the flushed tail before the report too; - using the v1 signal on v2; - removing the latch, either of its record resets, or the subagent condition. The exception is clearing the latch when a subagent finishes. That's memory cleanup that no test can observe, like the `streamedBlocksByParent.delete` beside it. - `npm run check`, build and all 2796 tests pass, with no golden changes. **Live, CLI 2.1.293 on Haiku, v1 and v2** - A plain cancel mid-answer, and cancels with a background command finishing during or after the cancelled turn. - Nothing of the cancelled turn arrived after its report. - Claude Code's follow-up after the cancel streamed in full. On v2 it came in its own `running` … `idle end_turn` cycle. - An abandoned prompt that Claude Code had already dequeued is a narrow race through the adapter, so I didn't reproduce it live. The tests follow the order a direct CLI probe recorded for agentclientprotocol#1309. ## Not changed - A dropped prompt that runs a tool can still surface it after the cancel, through `canUseTool` (tool call plus permission request) and the PreToolUse preview hook. Neither checks the cancel. A check there would read the consumer's state, which lags the CLI's control channel, so this needs its own design. - On CLIs without `request_marker_lists_v1`, the window still lasts until the next prompt's echo. Claude Code's own follow-ups in that window now lose their streamed copy as well as the consolidated one. The bundled CLI has the marker.
…e experimental ACP v2 surface (agentclientprotocol#1314) ## Summary When the user approves a plan through ExitPlanMode, a v2 client now gets it as the session's markdown plan. The update uses the draft's unstable plan operations: ```json { "sessionUpdate": "plan_update", "plan": { "type": "markdown", "planId": "plan-mode", "content": "# Plan…" } } ``` Until now a v2 client saw the plan only in the "Approve Plan" tool call and its permission request. ## Behavior - **Its own plan id.** The approved plan has the id `plan-mode`, and the TodoWrite checklist keeps `plan`, so the client shows both. Claude Code keeps one plan file per session and rewrites it on a later plan mode, so each approval replaces the plan before it. Nothing removes it. - **A normal approval** is a successful ExitPlanMode result. The plan follows the tool call's result, live and on replay. - **Live:** the plan comes from the tool's structured output, which includes edits the user made to the plan. - **Replay:** the SDK's `getSessionMessages` drops the structured output, but the transcript keeps the `plan` that Claude Code adds to the tool's input. - **An approval that clears the context** ends the tool with an error result. The restart into the fresh context publishes that plan, after the new mode and config options and before the fresh context runs. It does so only once that context exists, so a cancel or a failed restart leaves no plan behind that was never carried out. - **Not published:** a rejected plan, a subagent's plan (`parent_tool_use_id` or `isAgent`), a plan still waiting for its team lead, and an empty plan. Plans are trimmed. - **Who gets it.** The agent sends plan operations only to a client that advertises v1's draft `plan` capability, as the v1 draft requires. The v2 bridge advertises it for every v2 client and passes `plan_update` and `plan_removed` through unchanged, since the shapes match. No v1 client advertises `plan` today, so v1 behavior doesn't change. ## Replay limits - **After a "clear context" approval, replay doesn't show that plan.** It reads the transcript from before the clear, where the approval is the deny, so it shows the plan approved before that, or none. Replay never reaches the conversation after a clear, so this is part of an existing gap. - **A plan approved before a compaction boundary isn't replayed**, like the rest of the history before it. - **Replay can't apply the subagent and team-lead checks**, since they come from the structured output that `getSessionMessages` drops. A subagent's transcript still carries its `parent_tool_use_id`, so it's skipped. ## Evidence **Live, CLI 2.1.293 on Haiku, through the adapter on v2 in plan mode:** - **Normal approval:** the plan arrived right after "Exited Plan Mode", and resuming with replay republished it. - **"Clear context":** the plan arrived after the switch to auto mode, before the fresh context's first tool call. - **Raw SDK probe:** the structured result carries `plan`, `isAgent` and `filePath`, and the transcript's tool use keeps `plan` and `planFilePath`. **Tests and checks** - **Result path:** unit tests for each rule and for the order. They include structured output versus input, input-only as on replay, and structured output with no plan. - **Restart path:** coordinator tests check that the plan comes after the session state and before the continuation, that a client without the capability gets none, and that a failed restart publishes none. - **Agent:** a client that advertises `plan` gets the restart's plan, and one that doesn't gets nothing. - **v2:** a replay test and translation tests. The v2 `exit-plan-approve` golden gains the plan update. `exit-plan-reject` and every v1 golden are unchanged. - **Mutation checks:** removing any rule on either path fails at least one test. That covers both capability checks, the agent's capability wiring, success only, structured output first, the order, trimming, and the `plan_removed` pass-through. - `npm run check`, build and all 2810 tests pass.
…tclientprotocol#1317) ## Problem The v1 schema says a forked session is active once the agent responds, and can be prompted without `session/load` or `session/resume` (agentclientprotocol/agent-client-protocol#2359). `session/fork` only copied the transcript, so prompting the new id failed with "Session not found" until the client loaded or resumed it. ## Change - **The fork starts the new session the way a resume does.** It starts from the copied transcript, with the model and the permission mode that transcript records, and with the request's `cwd`, MCP servers, additional directories and `_meta`. The response carries the session's `modes` and `configOptions`, and the available commands follow it, as for `session/new`. - **A client that loads or resumes after a fork keeps working.** A later `session/load` replays the history into the running session, and a later `session/resume` returns it. Neither starts a second Claude Code process. A load or resume with other settings restarts the session, as for any loaded session. The fingerprint that decides this ignores the AIR fork point in `_meta`. - **Cost:** each fork now starts a Claude Code process. That added about 0.65 s to the fork response in the live run. - **v2:** the v2 fork used to resume the forked session itself. It now uses the session that the v1 fork starts, so the v2 bridge drops its own resume. - **AIR docs:** `docs/air-extensions.md` now says, under the session fork point, what a load or resume after a fork does. ## Evidence **Live, CLI 2.1.293 on Haiku, over v1:** 1. A new session answered "Remember the codeword PELICAN" with `OK`. 2. `session/fork` returned the fork with mode `auto` and the `mode`, `model` and `effort` config options. 3. Prompting the fork right away returned `PELICAN`, with no load or resume in between. 4. A `session/load` of the fork then replayed its history and was ready in 4 ms. The timing log shows one session start for the fork and none for the load, and a further prompt still answered. **Tests:** - New: the fork can be prompted at once, and its response carries modes and config options; a later resume and load reuse the session; and the fork fails when its session can't start. - The existing fork tests now also check that the session starts from the fork, with the request's additional directories. - The v2 fork tests pass unchanged. - **Mutation checks:** removing the start, the response's `modes`, or the additional directories each fails a test. - `npm run check`, build and all 2813 tests pass, with no golden changes.
… on the experimental ACP v2 surface (agentclientprotocol#1318) ## Problem A v1 report's `_meta` belongs to that report, and the agent gives each report its own `_meta.claudeCode` keys. A v2 client stores a tool call's `_meta`, and any report that carries `_meta` replaces it (`tool-calls.mdx`; the migration guide since agentclientprotocol/agent-client-protocol#2355). So a v2 client kept only the last report's keys. For example, a result's `{ toolName }` replaced the denial reason that came before it. On top of that, a PostToolUse hook stored the whole `tool_response` in `_meta`, which for a Write is the whole file text. ## Change All of this is v2 only. No v1 or AIR output changes, and no v1 golden changes. - **Stored `_meta` (`src/v2/tool-call-meta.ts`).** The v2 bridge keeps each tool call's `_meta` the way the client stores it. `claudeCode` keys merge one by one, and every other key replaces whole, the same model `ToolCallFieldTracker` uses. - A report that changes the stored `_meta` carries all of it. - A report that changes nothing carries no `_meta`, which leaves the stored value unchanged. - A permission request's subject always carries the stored `_meta`, since a client can show the request on its own. - The tool call's first report starts from nothing. - **`toolName` becomes `name`.** `claudeCode.toolName` repeats the tool's name, which v2 has a field for. It becomes the `name` of a tool call that has none yet, and is left out otherwise. A memory recall gets a `name` this way. Subagent routing still reads `toolName` on the agent's v1 reports, before the bridge. - **The `toolResponse` AIR gets.** A v2 client also keeps `_meta`, so it gets AIR's trimmed `toolResponse`: - a hook carries only the `status` and `isAsync` markers, not output the content already shows; - a denial carries the SDK message only when it differs from the reason. ## Evidence - **v2 goldens:** 28 change. Most only lose `toolName`. Hook reports lose the repeated stdout or file text. Memory recall gains `name`. The denial keeps its reason type and message. Subagent tool calls and their permission subjects keep `parentToolUseId`. - **Live, CLI 2.1.293 on Haiku, v2:** a Write and a Bash command arrived with `name` on their first report and no `_meta` at all. - **Tests:** 7 unit tests for the stored `_meta`. - **Mutation checks:** removing any of these 9 rules fails at least one test: - naming once; - leaving `toolName` out; - merging with earlier `claudeCode` keys, which also fails the `subagent-late-child-update` golden; - keeping other keys; - omitting unchanged `_meta`; - starting fresh; - the subject's stored `_meta`; - the bridge wiring; - the trimmed `toolResponse`. Forgetting a closed session's tool calls is memory cleanup that no test observes, like `V2Terminals.forget`. A limit of 2048 tool calls also bounds the memory. - `npm run check`, build and all 2820 tests pass.
…ientprotocol#1266) ### Summary Addresses [IJAI-2956](https://youtrack.jetbrains.com/issue/IJAI-2956). - Recognize Central only when the active key source is `apiKeyHelper` and its effective helper command contains the whole word `central` and the word sequence `proxy start`, case-insensitively, allowing multiple whitespace characters. - Read the effective command through the existing `SettingsManager`/SDK settings resolution, including project/managed overrides, rather than reimplementing configuration merging or executing helpers. - Return `kind: gateway` and the exact label `JetBrains Air Gateway`, without `detail` or invented account identity. URL, flags, routes, and endpoint ownership are intentionally not checked: this is an agreed weak display heuristic, not proof of provenance or entitlement. - Render ordinary helpers as `API key helper` instead of raw `apiKeyHelper`; do not expose unknown key-source identifiers. Preserve external backend and client-owned configuration priority, CLI/SDK merging, stale-probe protection, and duplicate suppression. - Add mapper and ACP-agent regression coverage for positive/negative commands, overrides, CLI/SDK results, and source changes; update `docs/air-extensions.md`. Examples matching the heuristic include `central proxy start` and `/opt/bin/CeNtRaL PrOxY StArT --arbitrary-flag`. `decentralized proxy start`, `central start`, and an inactive helper do not match. ### Sanitized auth status ```json { "kind": "gateway", "label": "JetBrains Air Gateway" } ``` No helper command, key, header, private route, or invented account information is sent. ### Verification Based on fresh upstream `main` at `0724b17`, rechecked immediately before publication. Changes and rebuilt `dist` are available in the main local clone; the user confirmed the local adapters work. ```bash npm run build npm run lint npm run test:run ``` - Build and lint passed. - Full test suite: **2484 passed, 30 existing skipped** across 44 passed test files and 4 skipped files. - No new skip flags or disabled tests were used. ### Scope No AIR UI, provisioning, personal configuration migration, effort/mode changes, or new ACP protocol. Plan files and personal configurations are not committed. --------- Co-authored-by: Junie <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Andrey Bragin <[email protected]>
… are not AIR (agentclientprotocol#1320) The first of three stacked PRs that replace agentclientprotocol#1257, split for review. Together they bring v1 clients the [subagents RFD](https://github.com/agentclientprotocol/agent-client-protocol/blob/main/docs/rfds/subagents.mdx), as revised by agentclientprotocol/agent-client-protocol#2281 and #2308. **AIR is unchanged**: its golden files don't change. 1. **This PR:** the RFD form for a v1 client that declares `subagents` and isn't AIR. 2. agentclientprotocol#1321: a child keeps its session across SendMessage resumes. 3. agentclientprotocol#1322: a child reports `requires_action` while its permission or question request is open. Merge them in that order. Each later PR is based on the previous branch, so its diff shows only its own part. ## Who gets what - **AIR** (any client that declares `_meta.jetbrains.air`): the earlier draft it implements, exactly as before. That's `subagent_spawned`, `subagent_state_update`, a session per generation, the hidden Agent/Task tool call, and draft replay. AIR keeps it whichever signal it declares. - **A v1 client with `clientCapabilities.subagents` that isn't AIR:** the RFD form below. SDK 1.8.0 types it (`subagent_update`, `session_message`, the v1 `StateUpdate`). - **Without either signal:** nothing changes. `subagentForm` (`src/acp-subagents.ts`) picks the form. `NativeSubagentRuntime` keeps the routing, buffering and nested lineage, and renders either form. ## The RFD form | RFD | Before | Now | | --- | --- | --- | | Announce the child before any of its traffic | `subagent_spawned { name, task, prompt }` | `subagent_update { sessionId, title, description, state: { state: "running" } }` | | `description` is the role or purpose, "not a transcript of instructions" | `task` was the prompt | `title`: the agent's `name`, with its role (`subagent_type`) after it, else the role. `description`: the Agent input's short `description` | | The parent's message to the child is an incoming `session_message` | `prompt` on the announcement | a `session_message` in the child's transcript, with the Agent/Task tool use's id and the session that made the call as sender | | Delegation tools stay tool calls | the Agent/Task tool call was hidden | it stays a tool call of the session that made it, with the report its model got. Its heartbeats and result stay there too | | Work-state snapshots, not a terminal lifecycle | `completed`, `failed`, `cancelled`, `disconnected` | `running`; `idle` with `end_turn`, `cancelled`, or `error` with the SDK's error text as its JSON-RPC `error`; `idle` with no stop reason for work that ended without a known reason (a worker restart, or still running when the stream ends) | | `idle` isn't terminal | later updates were dropped | an idle child still gets its updates, such as its background work's | | A cancel stops the work it waits on; other descendants are up to the Agent (#2308) | `session/cancel` ended every child as cancelled | the adapter ends no child itself. Claude Code's interrupt stops the foreground children, and currently background ones too; each reports `idle`/`cancelled` from the SDK's events, and the foreground ones' requests are withdrawn | | Never guess a parent | a child that ended before its spawning frame was announced under the root | it stays unexposed. A nested child waits for its parent's announcement | | Never move a tool call | | a tool call that a permission request emitted on the root before its child was announced stays on the root after a cancel. AIR keeps the cancel's cleanup, since its draft ends every child there | | No agent-side capability | `sessionCapabilities.subagents` for every client | for AIR only | ## Not in this PR - **SendMessage resumes** (agentclientprotocol#1321). Until then a resumed child stays `idle`. Requests find a child by its agent id, so only the permission and question requests of the resumed run appear in its session. A resume never opens a second session for the child, which the RFD forbids, and a test pins that. - **`requires_action`** (agentclientprotocol#1322). - **Replay** doesn't expose children yet. Today's replay invents `<session>:replay-subagent:<id>` ids, and the RFD requires the original ones. The RFD makes child replay a SHOULD, so an RFD client's replay leaves children out until the replay PR. ## Settled during the review of agentclientprotocol#1257 - **Cancel cascade:** the RFD leaves background children to the Agent (#2308). An agent-level test pins #2308's validation scenario. - **Session ids across forks:** the raw SDK agent id stays the session id. The SDK's fork copies no subagent history, so a fork can't repeat an id under another root. The reasoning is on `nextChildSessionId`. - **Held turns:** the adapter holds a v1 prompt open while a background subagent the turn started runs. A cancel still settles the prompt. ## Live run Against real Claude Code (Sonnet), with a v1 client that declares `subagents` and checks every update and permission request against the SDK's v1 schema, there were no schema violations: - **Launch:** `running` with the title (`scout (general-purpose)`) and description, the prompt as a `session_message` from the root, and the child's tool calls in its session. Then `idle`/`end_turn` before the Agent call completes on the root. - **A 40-second child:** the Agent call's heartbeat and result stay on the root. Before the fix, they moved to the child, because the heartbeat named the Agent call as its own parent. - **Cancel:** a foreground child is `idle`/`cancelled` 15 ms after `session/cancel`, and the prompt returns `cancelled`. Claude Code stopped a background child too (its metadata says `stoppedByUser`). - **Found on `main`, for every client, and fixed separately:** after a cancel, a tool call that was running never gets a final status. ## Tests - **AIR:** the end-to-end tests of the earlier draft run with an AIR client that declares `nativeSubagentSessions` (`AIR_NATIVE_SUBAGENTS`). The AIR golden files are unchanged. - **RFD, end to end:** - announcement and prompt - a failure with the SDK's error, also when the error arrives after the failure - a stop, and a worker-restart orphan - no draft updates - the cancel per #2308 - the raced tool call after a cancel, kept on the root, with AIR's cleanup kept - the Agent call's heartbeat Every update is checked against the SDK's v1 schema. - **RFD, runtime:** - the visible control tool call, also nested - labels, also from a late frame - an unparented child staying unexposed, and a nested child announced only after its parent - nothing reported at the parent's cancel - the end of the stream, and `unknown` - a resume opening no second session - **Plain/Zed comparison with origin/main:** it now allows the missing agent capability, which `compare.ts` documents. - **Mutation checks during agentclientprotocol#1257's reviews:** removing any of these rules fails tests. - **Checks:** `npm run check`, build and all 2863 tests pass. ## Release Titled `feat:`, because it changes what a v1 client that declares `subagents` receives.
…nts RFD clients (agentclientprotocol#1321) The second of three stacked PRs that replace agentclientprotocol#1257. It's based on agentclientprotocol#1320, so its diff shows only the resume part. Merge agentclientprotocol#1320 first; this PR then retargets to `main`. 1. agentclientprotocol#1320: the RFD form for a v1 client that declares `subagents` and isn't AIR. 2. **This PR:** a child keeps its session across SendMessage resumes. 3. agentclientprotocol#1322: `requires_action` while a child's permission or question request is open. **AIR is unchanged:** it keeps a new session per generation, as in its draft. ## What changes The [subagents RFD](https://github.com/agentclientprotocol/agent-client-protocol/blob/main/docs/rfds/subagents.mdx) gives a child one session id across every delegation to it. With agentclientprotocol#1320 alone, a resumed child stays `idle`. | RFD | With agentclientprotocol#1320 | Now | | --- | --- | --- | | One session id per child, across delegations | a resumed child stays `idle`, and its resumed work isn't shown | the child runs again in the same session, under the SDK agent id. The CLI runs a resumed agent under the SendMessage call's tool id, so that call becomes the child's current delegation: the resumed run's output and end reach the child | | The parent's message to the child is an incoming `session_message` | only the launch prompt | each SendMessage that resumes the child is a `session_message` with the SendMessage tool use's id. Its sender is the session that made that call, or left out when unknown. A repeated resume signal doesn't repeat it | Out-of-order events, from the reviews of agentclientprotocol#1257: - **Late end:** the end of an earlier delegation ends nothing (`isStaleEnd`). It also no longer settles the resumed run's live task at the agent level. - **Late result:** a SendMessage result that arrives after its run ended resumes nothing (`isEndedDelegation`). - **Early output:** a resume's output that arrives before its `task_started` is buffered, then delivered to the child. - **Reused id:** a run under an id the child used before ends the run, since the SDK falls back to it. - **Running patch:** a `task_updated` running patch reports `running` without its guessed prompt or call. ## Live run Against real Claude Code (Sonnet), a SendMessage to a finished child ran it again in the same session. Its message arrived with the SendMessage's id and the root as sender, and the turn waited for the resumed run. No schema violations. ## Tests - **End to end:** - a SendMessage resume under the SendMessage's tool id: one session, two prompts, the resumed output in the child, a stale end ignored - the resumed run's live task kept when an earlier delegation's end arrives late - a late SendMessage result - **Runtime:** - reuse and resume - a resume's output buffered before its `task_started` - the sender of a resume's prompt: the root, a child, or unknown - a run under a reused id - a running patch - a result without a text - **Mutation checks during agentclientprotocol#1257's reviews:** removing any of these rules fails tests. - **Checks:** `npm run check`, build and all 2873 tests pass on this branch. CI runs only on PRs to `main`, so I ran `ci.yml` on this branch by hand.
…for subagents RFD clients (agentclientprotocol#1322) The third of three stacked PRs that replace agentclientprotocol#1257. It's based on agentclientprotocol#1321, so its diff shows only the `requires_action` part. Merge agentclientprotocol#1320 and agentclientprotocol#1321 first; this PR then retargets to `main`. 1. agentclientprotocol#1320: the RFD form for a v1 client that declares `subagents` and isn't AIR. 2. agentclientprotocol#1321: a child keeps its session across SendMessage resumes. 3. **This PR:** `requires_action` while a child's request is open. **AIR is unchanged.** ## What changes The [subagents RFD](https://github.com/agentclientprotocol/agent-client-protocol/blob/main/docs/rfds/subagents.mdx) reports a child that waits for the user as `requires_action`. - **While a request is open:** a child whose permission or question request is open reports `requires_action` on its parent, then `running` again when its last open request ends (`NativeSubagentRuntime.awaitingUser`). The agent wraps the permission and elicitation requests of a child session in it (`withChildAwaitingUser`). - **Overlapping requests** keep `requires_action` until the last one ends. A child that ended while it waited doesn't report `running` again. - **A resume while a request is open** reports `requires_action`, not `running`. ## Live run Against real Claude Code (Sonnet), a child's Bash permission request showed `running` → `requires_action` while the request was open → `running` once it was answered. No schema violations. ## Follow-ups (low severity, not in this PR) - **Brief `running` after a withdrawal:** after a cancel withdraws a child's request, the child briefly reports `running` before the SDK's stop event. The same happens at a stream end, whose cleanup aborts every pending request. - **Ignored cancel requests:** AskUserQuestion's `createElicitation` is not raced against the abort. If a client ignores `$/cancel_request`, the child's open-request count stays up, and a later resume reports `requires_action`. ## Tests - **End to end:** `requires_action` around a child's permission request. The cancel test now also expects the background child's `requires_action` before the cancel. - **Runtime:** overlapping requests, a child that ended while it waited, and a resume with an open request. - **Checks:** `npm run check`, build and all 2877 tests pass on this branch, whose tree is agentclientprotocol#1257's with `main` merged in. CI runs only on PRs to `main`, so I ran `ci.yml` on this branch by hand.
…gentclientprotocol#1324) ## Problem A PostToolUse hook update skipped the subagent router, for every client with native subagent sessions (AIR's draft and the subagents RFD form): - **A child's Edit or Write:** its final diff went to the **root** session, with `parentToolUseId` set. All its other reports go to the child session, so the client got an update for a tool call it doesn't have on the root, and the child's tool call never got its final diff. The RFD forbids moving a tool call between sessions. - **AIR's hidden Agent/Task call:** AIR's draft hides the Agent/Task tool call, but its hook update reached AIR anyway, for a tool call AIR never received. AIR's golden files had recorded both. ## Cause `toAcpNotifications` registers a tool's PostToolUse hook the first time it sees the `tool_use`, with the client passed to it. Claude Code streams, so that first time is a stream event, and the stream path passed `this.client` instead of the routed client that every other path uses. The hook's update then went straight to the client. ## Change The stream path passes the routed client, so a hook update goes through `sendUpdate` and the subagent router like every other update. That client is the only one the stream path uses. Clients without native subagent sessions are unchanged: the router passes their updates through as they are. ## Evidence - **New test:** a child's Edit that is streamed, completed, and then gets its PostToolUse hook. All the Edit's reports, including the hook's diff, go to the child session. Without the fix, the hook's report goes to the root (`Set{ 'agent-42', 'test-session' }`). - **AIR golden files**, both intended: - `subagent-task-legacy`: the child Edit's hook update (`locations`) moves from `<session>` to `agent_1`, and the hidden Task call's hook update is gone. - `subagent-agent-async`: the hidden Agent call's hook update is gone. In both cases it was the only report AIR got for that tool call. - **How it was found:** declaring subagents for ACP v2 clients (the next PR) runs the RFD form through the v2 golden scenarios, whose check that a tool call's first report has a title caught the root-session diff. - **Checks:** `npm run check`, build and all 2878 tests pass.
…col#1325) Adds subagents to the experimental [ACP v2](https://agentclientprotocol.com/protocol/v2/overview) surface. With them, it covers v2's baseline session model, so this is the `feat:` that announces v2 support. The README gains a section on how to enable it and what it serves. Based on agentclientprotocol#1324, a v1 fix that this PR's golden scenarios exposed. Merge agentclientprotocol#1324 first; this PR then retargets to `main`. ## Experimental ACP v2 Started with `CLAUDE_AGENT_ACP_EXPERIMENTAL_V2=1`, the adapter serves the draft ACP v2 to a client that asks for protocol version 2 or later in `initialize`, and v1 to every other client, as before. - **Served on v2:** - the session baseline (`session/new`, `list`, `resume` with replay, `close`, `prompt`, `cancel`) - `session/delete`, `session/fork`, and additional directories - config options (the mode is one), plans, notices, compaction - display terminals for commands - permission requests, elicitation, providers, auth - and now subagents - **v1 only for now:** steering and the AIR extensions. The draft can still change, and the surface changes with it. ## What changes The [subagents RFD](https://github.com/agentclientprotocol/agent-client-protocol/blob/main/docs/rfds/subagents.mdx) makes subagent updates part of v2's baseline: "support is assumed rather than negotiated". - **Every v2 client gets subagents.** The v2 bridge declares `subagents` in the v1 request it makes for a v2 client (`v1InitializeRequest`), so the agent reports each Agent or Task subagent in the RFD form from agentclientprotocol#1320–agentclientprotocol#1322. - **The updates pass through:** `subagent_update`, `session_message` and `session_message_chunk` have the same shape in v1 and v2. - **The child's state goes to its own stream too.** v2 reports a child's work state on the child's own stream, mirrored on the parent. So a `subagent_update` with a `state` also becomes the child's `state_update`, sent right after the parent's copy (`v2ChildStateUpdate`). The announcement thus comes first, and both copies of a state come before any later one, as the RFD requires. This covers `requires_action`, and a failed child's `idle`/`error` with its JSON-RPC `error`. - **A state the RFD doesn't define throws,** as an update kind the surface doesn't translate already does. The SDK types the open members of v1's and v2's `StateUpdate` apart, so the bridge checks each state against the RFD's before passing it on. - **Replay doesn't flatten a child's inline text onto the root.** On replay, a subagent's text goes to the root only for a client that gets it there live: one without native subagent sessions that asked for the flattened transcript. Before, an RFD client (v1, and now v2) got the inline subagent text of a transcript replayed on the root, though the RFD form doesn't replay children yet. Current Claude Code keeps subagent messages in `subagents/`, not inline, so only the `session-load-replay` scenario's inline message showed it. AIR's replay is unchanged. ## Live run Against real Claude Code (Sonnet), with a v2 client that checks every `session/update` and permission request against the SDK's v2 schema, there were no schema violations: - **Launch:** - the child is announced on the root, then reports its own `running`, then gets its prompt as a `session_message` from the root - its tool calls arrive on its session, each with a title on its first report - the Agent call's 35-second heartbeat stays on the root - **Permission:** - `requires_action` appears on the child's stream and on the parent's copy, and on the root, whose turn waits on the child - the request goes to the child's session - then the child is back to `running` - **SendMessage resume:** - the same child session runs again, with the root as sender - the root turn goes idle while the resumed child keeps running, and the child reports `idle`/`end_turn` later, which v2 allows for background work - **Cancel:** the child is `idle`/`cancelled` on both streams, then the root is `idle`/`cancelled`. - A tool call still running at the cancel never gets a final status. That's a known bug on `main` for every client, fixed in a separate PR. ## Tests - **End to end on v2:** an Agent call whose child fails with an SDK error. Traffic arrives in order on both streams, and the child's `idle` carries `error: { code: -32603, message }`. - **Unit:** - the pass-through of the three updates - the child's `state_update`, and no copy for an association update without a state - the throw for a state and a stop reason the RFD doesn't define - **v2 golden files:** - the six subagent scenarios now show the RFD form: announcements, prompts, child sessions, both copies of each state, and permission requests on the child session - `session-load-replay` is unchanged from `main` - the scenarios also check every message against the v2 schema, and that each tool call's first report has a title (that check found agentclientprotocol#1324) - **Mutation checks:** each of these fails tests: - not declaring `subagents` - dropping the child's copy - sending it before the parent's - dropping the state check - dropping the replay rule - **v1 golden files:** unchanged. - **Checks:** `npm run check`, build and all 2882 tests pass. CI runs only on PRs to `main`, so I ran `ci.yml` on this branch by hand. ## Release Titled `feat:`, so its title becomes the release note that announces v2.
…cel (agentclientprotocol#1327) ## Problem After `session/cancel`, a tool call that was still running never gets a final status. On `main` (Claude Code 2.1.293, a v1 client that declares no capabilities) all three of these stay `pending` for good: - the root's own Bash call, cancelled while its command runs; - a foreground Agent call; - the Bash call of that Agent's subagent, which this client sees as a root tool call with `_meta.claudeCode.parentToolUseId`. v2 has the same problem, and so does the subagents RFD form: the subagent's call stays pending in the child's session after the child reports `idle cancelled`. Found in the live run of agentclientprotocol#1257. ## Cause I recorded the raw SDK stream (`emitRawSDKMessages`) and the session transcripts of a live run on `main`. There are two gaps. 1. **The window drops Claude Code's interrupted result.** For the root Bash and the Agent call, Claude Code does send a result, inside the post-cancel window of agentclientprotocol#1309 and agentclientprotocol#1313, before the turn's `result`: a `tool_result` with `is_error: true`, stamped `non_execution_kind: "user-rejected"`, then `[Request interrupted by user for tool use]`, then the aborted result. The `if (session.cancelled) break;` in the `user`/`assistant` case drops it. 2. **The subagent's call gets no result at all.** Claude Code writes its interrupted result only to the subagent's own transcript (`<session>/subagents/agent-*.jsonl`). The stream never carries it, before or after the response, so no change to what the window delivers can reach it. Nothing else closes these calls. `closeUnfinished` runs only at `end_turn`, over the turn's `foregroundToolCallIds`, and that set holds none of the three: `task_started` removes a Bash or Agent call from it (`registerLiveTask`), and a subagent's call is never in it. ## Change Two rules, in the stream consumer: - **Deliver the real result.** Until the client is told that the stopped work ended (the prompt response on v1, `idle` on v2), the window delivers the `tool_result` of a tool call that the client holds open, with Claude Code's own text and `nonExecutionKind`. Only the tool call is updated. The other effects of a tool result (async tasks, plan state, the ExitPlanMode flow) stay dropped, and so does everything after that report, which is what agentclientprotocol#1313 closed. - **Close what has no result.** When a turn settles `cancelled` because of `session/cancel`, the adapter closes every tool call still open with `AcpToolCallRenderer.unfinished`, before it resolves the turn. That covers the subagent's call, a `tool_use` that Claude abandoned before it ran, and every call when the stream gives no results (the force-cancel backstop). Both protocols end the work at `turn.resolve`, so one rule serves v1 and v2. Supporting pieces: - The closing report names the subagent that made the call (`Session.toolCallParents`, bounded like `dispatchedToolCalls`), so the subagent runtime routes it with the call's other updates: to the child session in the RFD form (an idle child still takes updates), to no child that AIR ended, to the root for a client without subagents. - A **background** subagent that the SDK has not reported ended keeps its calls open. The RFD form lets a child run on through a cancel (`NativeSubagentRuntime.parentCancelled`). Two tests merged with agentclientprotocol#1320 failed against my first version, which closed those calls, and that is why the registry exists. A foreground subagent ends with the work that waits on it. - For a v2 client, an error result in the window is `cancelled`, whichever kind Claude Code stamps. CLI 2.1.293 stamps a command that the interrupt killed `user-rejected`, which `isAbortedToolResult` reads as a refusal (`failed`). A tool that finished as the cancel landed stays `completed`. A v1 client gets `failed`, as before, since v1 has no `cancelled`. - `unfinished` gets a `cancelled` variant: "The turn was cancelled before this tool call returned a result." - The rule applies to `session/cancel` only (`session.cancelled`), not to the ExitPlanMode "keep planning" outcome, which also ends a turn `cancelled`. - README: one sentence on what an RFD client gets at a cancel. ## Why both rules | | root Bash | Agent call | subagent's Bash | | --- | --- | --- | --- | | Deliver the real result only | fixed | fixed | **still pending** (no result in the stream) | | Close every open call only | fixed, made up | fixed, made up | fixed, made up | | Both (this PR) | Claude Code's result | Claude Code's result | made up | Closing alone would fix all three, but it throws away a result that Claude Code already sent: its text, and the `nonExecutionKind` that lets a client tell a rejection from an interruption. So the real result goes out where one exists, and the adapter makes up a status only for what Claude Code never reports. The Agent call ends first, then its subagent's call, in the same turn and before the response. ## What does not change - Output after the report stays dropped. A test pins that the tool of a prompt the cancel abandoned, which a permission request announces after the response, gets no result (it fails with the `workEndReported` check removed). - A result for a call the client does not hold open sends nothing and logs nothing. - AIR ends every child at the cancel, and the runtime drops every later update to an ended child. The closing reports go through the same routing and are dropped there, so nothing reaches an ended child (the live AIR run logs one `ignoring late update for terminal subagent`). - No golden file changes. None has a cancel, and the diff touches none. ## Evidence **Live** (Claude Code 2.1.293, Sonnet, each tool call's statuses before and after the prompt response; the probe allows each permission request after 1.5 s and cancels 4 s later, with `python3 -c "import time; time.sleep(30)"` so the command runs; every update is checked against the SDK's v1 schema): | | root Bash | Agent call | subagent's Bash | after the response | | --- | --- | --- | --- | --- | | `main`, plain v1 | `pending` | `pending` | `pending` | 0 updates | | this PR, plain v1 | `failed`, Claude Code's text | `failed`, Claude Code's text | `failed`, closed here | 0 updates, 0 schema violations | | this PR, RFD client | `failed` | `failed` | `failed`, in the child's session after its `idle cancelled` | 0 updates, 0 violations | | this PR, AIR | `failed` | hidden, as before | none: the child ended, and the runtime dropped the closing update | 0 updates | | this PR, v2 | `cancelled` | `cancelled` | `cancelled`, in the child's session, before the root's `idle cancelled` | 0 updates | The plain and v2 runs are on this commit. The RFD and AIR runs were on this change before it was rebased onto agentclientprotocol#1324 and agentclientprotocol#1325, which change the v2 subagent surface and the routing of a subagent's hook update, and not the cancel path. In the raw stream, the subagent's Bash has `task_notification stopped` and no result. Its transcript has the `is_error` result. **Tests** (16 new, one rewritten, one extended), scripted in the order recorded live: - `incomplete-tools.test.ts`, plain client: the real result of a running tool; an Agent call and its subagent's call; the force-cancel backstop; a start that streams in after the cancel; a result after the response; a stray result; an abandoned prompt's tool; a background subagent's call left open; a call that a permission request announced and a later update attributed; the registry's cleanup. - `acp-agent.test.ts`: the RFD form (the stopped child's call closed in its session, the running child's left open, the Agent call's result) and AIR (nothing to an ended child, including a permission-announced call). - `acp-v2.test.ts`: `cancelled` for the Agent call and its subagent's call before `idle cancelled`, `completed` for a tool that finished, and a cycle that Claude Code started. - `preserves cancellation and does not attribute its unfinished tool to the next turn` asserted that the cancelled turn's tool gets no `failed` update, which is the behavior this PR changes. It now asserts one report, before the cancelled response and not attributed to the next turn. **Mutation checks.** Each of these fails at least one test: no delivery of the real result (8), no closing at the settle (10), delivery after the report (1), delivery for calls the client does not hold open (1), closing a running background subagent's calls (3), no parent on the report (3), v2 real result not `cancelled` (2), `unfinished` not `cancelled` on v2 (1), the closing text (4), `ran` ignored (2), the registry kept after a call ends (1, added after the first run showed that nothing caught it), the parent recorded only from a `tool_call` (1), the v1 end-of-work signal on v2 (3), AIR's drop of updates to an ended child removed (5). `npm run check`, `npm run build` and `npm run test:run` (2898 tests) pass. ## Related, and not covered - agentclientprotocol#1221 closes the turn's `foregroundToolCallIds` at a cancelled settle with a made-up `failed`. It overlaps with this change in `settleActive` and in the test above, so the two will conflict. As far as I can tell from `registerLiveTask`, it would not have closed the three calls here. Its scenario, a start that streams in after the cancel, is covered by a test here. agentclientprotocol#1221 also hooks the inline settle of a held turn in `cancelTurns`, which this PR does not touch. - Not covered: a background subagent's calls when a cancel stops it while no turn settles (a cancel while idle, or of a held turn that `cancelTurns` settles inline); the calls of a v2 cycle that Claude Code started get Claude Code's results, but nothing closes the ones it sends none for. - The closing report carries no terminal exit, so a client that shows command output sees the command's call end without one. `closeUnfinished` does the same at `end_turn`. - A background subagent whose end the SDK reports only after the turn's result keeps its calls open. Claude Code 2.1.293 reports it before.
… at its end (agentclientprotocol#1344) A model cycle that Claude Code starts on its own now closes, at its end, the tool calls that Claude abandoned before they ran, as a turn's end already does. ## The bug Claude can start a tool use and never finish the message that holds it, for example when the stream is cut off. The tool never runs, and no result comes. At a turn's end, `settleActive` closes such a call (`closeUnfinished`: "Claude stopped this tool call before it ran.", `failed` on v1, `cancelled` on v2). A cycle that Claude Code started on its own, such as its follow-up after a background command finished, has no turn. So nothing closed its abandoned calls, and the client kept them open. ## The fix `closeCancelledCycleToolCalls` becomes `closeCycleToolCalls` and runs at every end of a cycle: its result, or an idle that no result precedes. - **After a cancel:** unchanged (agentclientprotocol#1339, agentclientprotocol#1342). - **At any other end:** every call that ran already has its result. What is open is a call of the main conversation that was never dispatched, which is closed with the same report a turn's end sends. A subagent's calls are not the cycle's: a background subagent runs on. On v2 the report comes before the cycle's `idle`. ## Evidence **Tests:** - v1, a cycle that runs a command, starts a background subagent that is streaming a tool use as the cycle ends, and abandons a tool use: only the abandoned call is closed, once. The command keeps its one result, and the subagent's call stays open. - v1, a cycle that ends at an idle with no result (the stream dropped mid-cycle): its abandoned call is closed there. - v2: the abandoned call goes `pending`, then `cancelled`, before the cycle's `idle end_turn`. - **Mutation checks:** each of these fails at least one test: - no closing at a normal end; - closing a subagent's calls; - closing dispatched calls (an existing test catches it); - closing only at the result, not at the idle. `npm run check`, build and the full suite (2982 tests) pass, with no golden changes. **Not checked live:** Claude abandons a tool use only when something cuts its message off, such as a steer or a dropped stream, which a live run cannot set up reliably.
…thdraws, on ACP v2 (agentclientprotocol#1346) ## Problem On ACP v2 the adapter reports a session's foreground work as `state_update`s: `running`, then `requires_action` while a permission request or question that blocks the work is open, then `running` again, and finally `idle` with a stop reason. A request that ends because `session/cancel` or `session/close` stopped the work is not an answer, but it still reported `running`. agentclientprotocol#1337 fixed that for a subagent's child session and listed the session's own request under "Not covered". That request did not change, and since agentclientprotocol#1338 neither did its AskUserQuestion, which now ends at the abort too. On `main`, with Claude Code 2.1.296, v2, a Bash permission request that the client holds open, and then a cancel or a close: | What stops the work | States the session reported | | --- | --- | | `session/cancel` | `running`, `requires_action`, **`running`**, `idle cancelled` | | `session/close` | `running`, `requires_action`, **`running`**, `idle cancelled`, all before the close response | Claude Code's interrupt withdraws the request, so the SDK aborts the signal of the `canUseTool` call, the request's `finally` calls `syncAwaitingUser`, and with no blocking request left it reports `running`. The result that ends the work comes 2 to 9 ms later in these runs, so the client shows work that is being stopped as working. ## Cause `syncAwaitingUser` reported `running` whenever the last blocking request ended, whatever ended it. A request ends when the user answers it or when Claude Code withdraws it, and a withdrawal alone does not say whether the work was stopped. A `PermissionRequest` hook that answers also makes Claude Code withdraw the request, and the work goes on (9 s more in a live run), as agentclientprotocol#1337 found. So "a withdrawn request reports no `running`" is wrong. The rule that fits is agentclientprotocol#1337's: no `running` when the request was withdrawn after a cancel that came while it was open. ## Change - `cancelTurns` (called for `session/cancel`, and by the teardown of `session/close`) marks each request that is open, in `Session.openUserInputs`. - `withPendingUserInput` takes the request's `signal`. When the request ends with its signal aborted and the mark set, `syncAwaitingUser` stops waiting but reports no `running`. The four callers pass their signal: permission requests, AskUserQuestion, MCP elicitation, and the refusal fallback prompt. (agentclientprotocol#1337 counts the parent's cancels and compares the count when the request ends. Here each open request is already an entry of `openUserInputs`, so the cancel marks it. Same rule.) - Everything else is as before: - an answered request reports `running`, also after the cancel; - a withdrawal without a cancel reports `running`; - with two requests open, only the last one to end reports, by how that one ended; - a request that opens after the cancel is judged on its own, so a hook that withdraws it still reports `running`. - A foreground subagent's request blocks the turn, so it behaves like the turn's own. A background subagent's request does not block it (`blocksForeground`), so the root's states do not change for it. agentclientprotocol#1337's child states are unchanged. - `ForegroundEvents.running` and `v2Foreground` say when `running` is not reported. v1 reports no session state, so nothing changes there. AIR is unchanged: the scenario golden files do not change. ## Evidence Live runs against Claude Code 2.1.296 (Sonnet), driving `dist/index.js` over stdio as a v2 client (`CLAUDE_AGENT_ACP_EXPERIMENTAL_V2=1`) that holds the request and ignores `$/cancel_request`, with the session's cwd in default permission mode. Every update and request validated against the SDK's v2 JSON schema: 0 violations in all runs. The root's states: | Run | `main` | This PR | | --- | --- | --- | | `session/cancel`, Bash permission request | `running`, `requires_action`, `running`, `idle cancelled` | `running`, `requires_action`, `idle cancelled` | | `session/cancel`, AskUserQuestion | `running`, `requires_action`, `running`, `idle cancelled` | `running`, `requires_action`, `idle cancelled` | | `session/close` | `running`, `requires_action`, `running`, `idle cancelled` | `running`, `requires_action`, `idle cancelled` | | `session/cancel`, request of a foreground subagent (agentclientprotocol#1337's run) | root as above; child `running`, `requires_action`, `idle cancelled` | root `running`, `requires_action`, `idle cancelled`; child the same | | `PermissionRequest` hook allows after 3 s, no cancel | `running`, `requires_action`, `running`, `idle end_turn` | the same | | the client answers after 2 s | `running`, `requires_action`, `running`, `idle end_turn` | the same | | `kill -9` of Claude Code, no cancel | `running`, `requires_action`, `running`, `idle error` | the same (see below) | The hook run is why a withdrawal alone cannot mean "stopped": Claude Code withdrew the request 3 s in, and the command then ran 9 s more. A v1 run of the cancel (no state updates there) is identical on both builds. Tests, in `acp-v2.test.ts`, through the v2 client app, which validates every update: - For each of the four kinds of request (permission request, AskUserQuestion, an MCP server's elicitation, the refusal fallback prompt): a `session/cancel` withdraws it, a `session/close` withdraws it (the harness gets a `close` hook, as the SDK's cleanup of a closed query aborts open requests), and Claude Code withdraws it without a cancel. - The user answers after the cancel; a request that opens after the cancel and is then withdrawn; two requests open together (both withdrawn by the cancel, and one withdrawn and the other answered, in each order); a request of a cycle that Claude Code started. - Subagents: a foreground subagent's request also holds the root (an assertion added to agentclientprotocol#1337's test); a background subagent's request changes nothing of the root's states, alone and next to the turn's own request. 13 of them fail on `main` (12 new tests and the extended one); the other 8 new tests pass there and guard what must not change. Removal checks, each run against `acp-v2.test.ts`: | Removed or changed | Tests that fail | | --- | --- | | no rule: the end always reports `running` | 13 | | the cancel marks no request | 13 | | a mark is enough, the signal is not required | 2 (answered after the cancel) | | the simple rule "any withdrawal means stopped" | 5 (the hook cases, and the request after the cancel) | | `session.cancelled` in place of the mark of a cancel that came while the request was open | 1 (the request after the cancel) | | sticky: after one request was withdrawn by a cancel, no later request reports `running` | 1 (answers the one the cancel did not withdraw) | | permission requests do not pass their signal | 6 | | AskUserQuestion does not pass its signal | 3 | | MCP elicitation does not pass its signal | 2 | | the refusal fallback prompt does not pass its signal | 2 | `npm run check`, `npm run build` and `npm run test:run` pass (2999 tests passed, 30 skipped; the scenario golden files do not change). ## Not covered - **A stream end without a cancel.** If Claude Code exits by itself, the SDK's cleanup aborts every request before the consumer reads the end of the stream, and nothing at that moment tells it from a hook's withdrawal. Live (`kill -9` of the CLI, table above): `running` 2 ms before the `idle`, and the session is dead right after. Left as it is, for the reasons agentclientprotocol#1337 gives for children. - The rule assumes, like agentclientprotocol#1337's, that the work reports its end right after such a withdrawal. A turn always does (the force-cancel backstop ends a wedged one); a cycle with no active turn has no such backstop. - Open PRs agentclientprotocol#1344 and agentclientprotocol#1345 also change `src/acp-agent.ts`; this branch merges cleanly with each (checked with `git merge-tree`).
…entprotocol#1340) ### Summary Addresses [IJAI-1783](https://youtrack.jetbrains.com/issue/IJAI-1783). An organization can restrict the models its JetBrains gateway serves, but a session routed through `providers/set` offered the whole Claude Code catalogue, so the picker showed models the gateway then refused. `providers/set` now lists the gateway's models before it responds, and the model picker offers only the SDK models the gateway lists. - `anthropic`: `GET {baseUrl}/v1/models?limit=1000`, paginated by `has_more`/`last_id`, with the provider headers and `anthropic-version`. - `bedrock`: `GET {baseUrl}/foundation-models` (`ListFoundationModels`, `modelSummaries[].modelId`), the listing the JetBrains gateway answers on its Bedrock route. Only `anthropic.` ids count. The cross-region prefix `global` when listed, else that of the first listed id, becomes the session's `ANTHROPIC_BEDROCK_REGION_PREFIX`, so the CLI calls the models under the prefix the gateway serves; ids of other prefixes, and SDK rows resolved to another prefix, are dropped. - Rows match by `resolvedModel` (or `value`) without dates, `-vN:M` suffixes, `[1m]` hints and provider prefixes; the narrowing runs after the `availableModels` allowlist. When the gateway does not list the model a session runs (the dropped default, or a resumed or recreated session's last model), the session switches to the first listed one with `setModel`, so the CLI does not keep running an unlisted model. - The listing runs in the provider-update queue, so `providers/set` and `providers/disable` apply in the order they arrive; logout also clears a `providers/set` that is still listing. - A 4xx answer means the gateway has no listing: `providers/set` succeeds and nothing is narrowed. Any other failed listing (network error, 5xx, unexpected body, 10 s timeout) fails `providers/set` with `internal_error`, and the previous provider stays in effect. An invalid header or a base URL with credentials fails without repeating the value. A listing that matches no SDK model leaves the session with no model to offer: there is no model config option. - `vertex`, gateways set through `authenticate`, and native routing are not listed. Setting the same gateway again reuses its listing; `providers/disable` and logout drop it. - `docs/model-configuration.md` describes the narrowing. ### Not covered Background haiku requests, subagents, and a `/model` typed into the prompt choose their own model and are not checked against the listing. ### Verification ```bash npm run check npm run build npm run test:run -- --no-file-parallelism # 2964 passed, 5 failed, 30 skipped; all 5 in session-index.test.ts, flaky on main too ``` Tests stub `fetch` (`src/tests/setup-no-network.ts` answers an empty listing by default) and cover the listing requests and pagination, the Bedrock region prefix and its env, region-aware matching, the dropped default, the failing `providers/set`, and the empty picker. The JetBrains Bedrock gateway's `/foundation-models` answers `global.anthropic.claude-{haiku,opus,sonnet}-5-5` (seen through Junie, which lists the same route); this adapter has not yet been run end to end against it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <[email protected]>
…ld session (agentclientprotocol#1347) A client on the subagents RFD can now stop one subagent without cancelling the turn. Each running child is announced with `capabilities: { cancel: {} }`, and `session/cancel` with the child's session id stops that child's current work. ## What Claude Code does From direct SDK probes of `Query.stopTask(taskId)` and runs through the adapter (Claude Code 2.1.293 and 2.1.296): | Case | What Claude Code sends | |---|---| | Foreground subagent running a command | At once: `task_updated` `killed` and `task_notification` `stopped` for the subagent, `stopped` for its command's task, the command's interrupted result, then the Agent call's interrupted result (`non_execution_kind: "interrupted"`). The parent's turn goes on and ends normally. | | Background subagent | The same events and result, then Claude Code's follow-up cycle about the stop. | | Subagent with an open permission request | The request is withdrawn (its `AbortSignal` fires), and both calls get results. | | Subagent waiting on its own foreground subagent | The grandchild is stopped too, with its own `killed` and `stopped`, which is the cascade the RFD requires. | ## The change - **The grant:** `announceNativeSubagent` grants `cancel` to each live RFD child. A replayed child gets no capabilities, since it no longer runs (the RFD's freshness rule for reconnection). A routing-only ancestor that a restore announces gets none either. - **The cancel:** `cancel` checks first whether the session id is a child of one of its sessions (`NativeSubagentRuntime.cancelChild`), and if so calls `Query.stopTask` with the child's agent id. It sends no interrupt, so the parent's work goes on. A stop that Claude Code refuses is logged. - **The report comes from the SDK's events,** `killed` or `stopped`, which `finishTask` already turns into `idle` with `cancelled`. Accepting the stop is no evidence, as the RFD says, and a cancel that races with a child's end does not rewrite its outcome. - **Requests:** a request that the stop withdraws reports no `running` before `idle cancelled`. `awaitingUser` now counts cancels of the child and of its ancestors, as it counted the root's (agentclientprotocol#1337). An ancestor's cancel stops the foreground subagents under it. - **Unchanged:** AIR keeps its draft and grants nothing, and a cancel of a root session works as before. ## Evidence **Live, through the adapter** (Claude Code 2.1.296, Sonnet), every update checked against the schema, with no violations: | Run | Child | Parent | |---|---|---| | v1 RFD, foreground child running a command | `running`, `requires_action`, `running`, `idle cancelled` 9 ms after the cancel; its command `failed` | Agent call `failed`; turn ends `end_turn` | | v1 RFD, background child while v1 holds the turn | `idle cancelled`; its command `failed` | the held turn settles with `end_turn` | | v1 RFD, child with its permission request open | `requires_action`, then `idle cancelled`, with no `running` between | turn ends `end_turn` | | v2, foreground child | `idle cancelled` on the root's stream and its own | Agent call `cancelled`; root `idle end_turn` | No tool call was left open in any run. **Tests:** - `native-subagents.test.ts`: - `cancelChild` stops the child's task and reports nothing itself, and the SDK's `killed` reports `idle cancelled`; - no stop for a session that is no child, or for AIR; - no `running` for a request that the child's own cancel withdraws, or that its parent's cancel withdraws. - `acp-agent.test.ts`: `session/cancel` on a child calls `stopTask`, not `interrupt`, and the turn ends `end_turn`; a refused stop is logged; nothing happens for an unknown session id or an AIR child. - `acp-v2.test.ts`: the same over v2, with the child's `idle cancelled` on both streams. - **Golden files:** the six v2 subagent golden files gain `capabilities: { cancel: {} }` on each announcement, and nothing else. No AIR golden file changes. - **Mutation checks:** removing each of these fails at least one test: - the grant; - the routing to the child; - the child's cancel count; - the ancestors in the count; - the AIR exclusion; - the error handling. So does reporting `idle cancelled` as soon as the stop is accepted. `npm run check`, build and the full suite (3014 tests) pass. ## Not changed, and a decision - **A parent cancel still stops background subagents.** Claude Code does that because the adapter does not declare the SDK's `perTaskStopAffordance` option. Declared, an interrupt spares background agents and background shell commands (checked live: both ran to completion after the interrupt), as agentclientprotocol/agent-client-protocol#2308 prefers. Background shell commands would then outlive a cancel too, and ACP has no control to stop them, so declaring it is left for a separate decision. - **Order:** Claude Code sends the stopped child's command result right after its `killed` event, so the client sees `idle cancelled` a few milliseconds before that call ends. The RFD has the client mark the child's unfinished calls cancelled at that point and still accept later updates. - **On v2,** the stopped child's command ends `failed`, not `cancelled`: Claude Code's result for a subagent's command carries no non-execution kind.
…ients that can cancel them (agentclientprotocol#1348) Stacked on agentclientprotocol#1347, which gives each running child the `cancel` capability. Merge agentclientprotocol#1347 first; this PR's base is its branch. ## What changes On a client that gets the subagents RFD form (v1 with `clientCapabilities.subagents` and not AIR, and v2), `session/cancel` on the parent now spares background subagents. They keep running, as acp#2308 prescribes ("a background child that the cancelled work did not wait on may continue"), and the client can stop each one with its child's `cancel` from agentclientprotocol#1347. Foreground subagents that the turn waits on still stop. Plain v1 clients and AIR are unchanged: the interrupt still stops their background subagents, since they have no way to stop one. ## How - **The declaration.** Claude Code's interrupt kills background subagents and workflows unless the query declares `perTaskStopAffordance`. The SDK describes it as "this consumer renders a per-task stop control", and an RFD client now has that control. The adapter declares the option for those clients. It comes before `_meta.claudeCode.options`, so a client can turn it off. - **Workflows.** The declaration also spares background workflows (`local_workflow`). A workflow's agents are not subagent tasks and get no child session, so a client can't stop a workflow unless it is AIR with `asyncTasks`. When a session declares the option and the client has no such control, `session/cancel` stops each live background workflow with `Query.stopTask` after the interrupt. Workflows therefore behave as they did before. - **Background commands** survive an interrupt with or without the declaration (checked live and in the CLI code), so nothing changes for them. Where I read Claude Code 2.1.296's CLI: without the declaration, the interrupt runs a kill of every running background `local_agent` and `local_workflow` task. With it, the interrupt kills only observer tasks. Adapter interrupts name no prompt, so this runs on every `session/cancel`, including one with no turn running. The declaration's only other effect is on requests with `interrupt_if_running`, which the adapter never sends. ## Checks **Live, Claude Code 2.1.296, through the built adapter.** Every update was checked against the schema, and no run had a violation or left a tool call open. Survival was checked by whether the background command's process was alive 2 s after the cancel. | Client | Background work at the cancel | Result | |---|---|---| | v1 RFD | subagent running a command | Prompt `cancelled` 9 ms after the cancel. The child kept running, completed its command and reported `idle end_turn`. Claude Code's follow-up cycle then streamed to the root. | | v1 RFD | subagent with an open permission request | The request stayed open through the cancel. The client allowed it 5 s later, and the child ran the command and ended `idle end_turn`. | | v1 RFD | workflow | The workflow and its command stopped, with no follow-up cycle. Same as the plain client. | | v1 plain | subagent | Unchanged: the interrupt killed the child, and its command's tool call ended `failed`. | | v1 plain | workflow | Unchanged: stopped, no follow-up. | | v2 | subagent | Root `idle cancelled` 11 ms after the cancel. The child kept running and later reported `idle end_turn`, and the root went `running`, then `idle end_turn`, for the follow-up. | | v2 | workflow | Stopped, no follow-up. | **Direct SDK probes** decided the design: - With the declaration, an interrupt spares a background workflow. - Without it, the interrupt kills the workflow. - A `stopTask` on a workflow, before or after the interrupt, stops it and its command, and Claude Code starts no follow-up cycle, the same as when the interrupt killed it. **Tests:** - Option tests: the declaration for each client form, a caller's override, and when a cancel stops workflows. - Cancel tests: only workflows are stopped, after the interrupt and only when the flag is set. - Removing any of the eight rules fails a test. - `npm run check`, build and all 3020 tests pass, with no golden changes. ## Not covered - A paused background workflow (one Claude Code restored on a resume) would also be stopped at a cancel, where the interrupt left it alone. I didn't check whether such a workflow reports `task_started`. - Standard ACP has no control to stop background work that is not a session, such as a workflow or a background command. Only AIR has one, through its async tasks extension.
…gentclientprotocol#1351) Found by running the ACP TCK's v2 suite against the adapter (ACP-DELETE-201 and ACP-DELETE-203). The code is shared, so v1 clients get the fix too. ## The bug ACP says: "Deleting an already-deleted session, or a session that never existed, **SHOULD** succeed silently." For a client that is neither AIR nor a `sessionIndex` client, `session/delete` instead failed with `-32603` "Internal error" in three cases: - **A session this connection ran but never prompted.** Claude Code writes the transcript at the first prompt. So the adapter closed the live session, then the SDK delete threw "not found in any project directory". - **An unknown session.** The SDK delete throws the same "not found" error. - **An id that isn't a UUID.** The SDK delete throws "Invalid sessionId". ## The fix A new `deleteSessionIfStored` in `src/session-index/service.ts` handles every other client's delete: - An id that isn't a session id can't name a transcript, so it returns `{}` without calling the SDK. - The SDK's own "not found" means nothing is on disk, so that also returns `{}`. It's recognised by the existing `isSdkNotFound`. - Any other SDK failure, such as a permission error, still fails the request. The two AIR paths are unchanged, because `docs/air-extensions.md` documents their errors: `-32002` for an unknown session on a `sessionIndex` client, and the SDK's error for AIR without `sessionIndex`. The doc now describes the plain client's behavior. ## Checks - **ACP TCK v2** (`agentclientprotocol/acp-tck` at `fa00ccd`) against this build: ACP-DELETE-201 and ACP-DELETE-203 now pass, where both failed before. ACP-DELETE-202 is still skipped, because `session/list` doesn't show a session that was never prompted. - **Tests:** three new tests: a never-prompted live session, an unknown session, and an id that isn't a UUID. Removing either rule, or swallowing every error, fails a test. The existing delete tests now use UUID ids, which is what real sessions have. - **Suite:** `npm run check`, build and all 3068 tests pass, with no golden changes.
…d changes its settings (agentclientprotocol#1353) Found by running the ACP TCK's v2 suite against the adapter (ACP-ADDDIRS-202). The code is shared, so it also fixes v1's `session/load` and `session/resume`. ## The bug A `session/resume` or `session/load` can change a live session's settings: its `cwd`, additional directories, MCP servers, or the other settings in its fingerprint. When that happens, `getOrCreateSession` tears the live query down and starts a new one with `resume: sessionId`. Claude Code writes the conversation only at the first prompt, so for a session that was never prompted, the resume failed with `-32002` "Resource not found". The live session was already torn down by then, so the client lost a session it had a moment earlier. ## The fix The sign-out path already handled the same case: it tries `resume`, and on `resourceNotFound` it starts a fresh query under the same id with `reuseSessionId`. That logic is now one helper, `recreateLiveSession`, used by both: - **`getOrCreateSession`** uses it when the session was live. A session that was never prompted then starts a fresh query under its own id. It loses no history, because it had none, and it keeps its mode. - **A session that isn't loaded here** still resumes directly. If it has no stored conversation, that still fails with `-32002`, since the session really is unknown. - **`recreateSignedOutQuery`** keeps its own handling: if both attempts fail, it puts the old session object back. ## Checks - **ACP TCK v2** (`agentclientprotocol/acp-tck` at `fa00ccd`) against this build: ACP-ADDDIRS-202 now passes, where it failed before. ACP-ADDDIRS-201 and ACP-RESUME-201 to 205 still pass. - **Live v1 `session/load`**, Claude Code 2.1.296: create a session, then load it with a new additional directory before any prompt. - On `main`, the load failed with `-32002`. - With this fix, the load succeeds. A prompt then read a file that exists only in the new directory, and the transcript was saved under the same session id. - **Tests:** three new tests: - a live session that was never prompted starts a fresh query with its mode; - an unknown session still fails; - another failure of a live session's resume doesn't start a fresh query. Removing the fallback, applying it to every error, or applying it to sessions that aren't loaded each fails a test. The sign-out tests pass through the shared helper. - **Suite:** `npm run check`, build and all 3068 tests pass, with no golden changes. **Not covered:** a model chosen with `session/set_config_option` before the first prompt resets to the default when such a session is recreated, as it already did after a sign-out. The mode is kept.
…tree) (agentclientprotocol#1352) ## What Marks `session/list` and `_session/list/changes` rows whose session runs in a linked Git worktree that Claude Code itself created (`claude --worktree`, the `EnterWorktree` tool), as `_meta.jetbrains.air.agentWorktree: true`. A new agent capability `sessionWorktrees`, advertised together with `sessionIndex`, tells the client that rows carry the field, so a missing field means "not made by the agent" rather than "unknown". ## Why With `includeWorktrees`, a client gets sessions from every linked worktree with nothing to tell them apart. A worktree Claude Code made for a session belongs to the project the user started it from; a worktree the user made by hand is a separate checkout. Only the agent knows which is which. ## How - The rule reads the transcript's `worktree-state` records: the latest one wins; the session counts only when `worktreeSession` is non-null and `enteredExisting` is not `true`. `environment.isWorktree` is not used, since plain git detection is also true for user-made worktrees. - The record is parsed in the existing head/tail transcript scan, so no extra file reads. The row is marked only when its `cwd` is in a linked worktree (`<common-dir>/worktrees/<name>`), never for the main checkout or a submodule. - The row's `cwd` stays the session's own directory. The field is `true` or absent. Proposed as `SessionInfo.agentWorktree` plus a `agentWorktrees` capability marker in the session list extensions RFD: agentclientprotocol/agent-client-protocol#2323. Docs: `docs/air-extensions.md`. ## Tests `src/tests/session-index.test.ts`: marked worktree, `enteredExisting`, no record (user worktree), a later `null` record, main checkout, `_session/list/changes` rows, capability negotiation, and scan windows. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
Updates `@anthropic-ai/claude-agent-sdk` from 0.3.296 to 0.3.300 (parity
with Claude Code v2.1.300), and `@anthropic-ai/sdk` from 0.133.0 to
0.134.0.
No code changes needed. The new types are an optional `property_order`
on elicitation requests, `nextFireAt` on the CronCreate/CronList
outputs, and `owned` on Artifact outputs. We don't need any of them.
Fixes picked up that affect this agent:
- `importSessionToStore` (used for session forks) no longer skips
transcript rows containing U+2028/U+2029 on Node 24.
- Resuming an SDK session after a compaction no longer loses the recent
messages or the compaction summary in some cases.
- Background subagents in auto mode no longer send their final report
twice or merge it into another turn.
- Queued content-block prompts merged into one turn are now separated by
a line break.
- `ScheduleWakeup` now fires while a background task is running.
Behaviour change: `MAX_THINKING_TOKENS=0` (sent as `thinking: { type:
"disabled" }`) now runs adaptive thinking on models that can't turn
thinking off.
`npm run build`, `npm run check` and `npm run test:run` all pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
🤖 I have created a release *beep* *boop* --- ## [0.91.0](agentclientprotocol/claude-agent-acp@v0.90.0...v0.91.0) (2026-10-15) ### Features * keep background subagents running after a parent cancel, for clients that can cancel them ([agentclientprotocol#1348](agentclientprotocol#1348)) ([ba3c7be](agentclientprotocol@ba3c7be)) * let a client cancel one subagent with session/cancel on its child session ([agentclientprotocol#1347](agentclientprotocol#1347)) ([aeb69e7](agentclientprotocol@aeb69e7)) * mark session list rows of worktrees Claude Code made (agentWorktree) ([agentclientprotocol#1352](agentclientprotocol#1352)) ([1a1f1a0](agentclientprotocol@1a1f1a0)) * narrow models to what the providers/set gateway lists ([agentclientprotocol#1340](agentclientprotocol#1340)) ([e8eb9d3](agentclientprotocol@e8eb9d3)) * restore a subagent from before a reload when SendMessage resumes it, for subagents RFD clients ([agentclientprotocol#1332](agentclientprotocol#1332)) ([df21b3f](agentclientprotocol@df21b3f)) ### Bug Fixes * close the tool calls of a background subagent that the SDK reports stopped after a cancel ([agentclientprotocol#1336](agentclientprotocol#1336)) ([9a1b90f](agentclientprotocol@9a1b90f)) * close the tool calls of a cycle that Claude Code started and a cancel stopped ([agentclientprotocol#1339](agentclientprotocol#1339)) ([1fd217f](agentclientprotocol@1fd217f)) * close the tool calls that a cycle Claude Code started abandoned, at its end ([agentclientprotocol#1344](agentclientprotocol#1344)) ([594aa8c](agentclientprotocol@594aa8c)) * deliver the output of a background subagent that a cancel did not stop ([agentclientprotocol#1345](agentclientprotocol#1345)) ([bcc024d](agentclientprotocol@bcc024d)) * end the terminal of a command whose tool call is closed without a result ([agentclientprotocol#1335](agentclientprotocol#1335)) ([764f339](agentclientprotocol@764f339)) * end the tool calls of a cycle that Claude Code started and a cancel stopped, on v1 ([agentclientprotocol#1342](agentclientprotocol#1342)) ([b4788d6](agentclientprotocol@b4788d6)) * keep a live session that was never prompted when a resume or load changes its settings ([agentclientprotocol#1353](agentclientprotocol#1353)) ([0c8de13](agentclientprotocol@0c8de13)) * let session/delete succeed for a session without a transcript ([agentclientprotocol#1351](agentclientprotocol#1351)) ([fadbe18](agentclientprotocol@fadbe18)) * report no running for the session's own request that a cancel withdraws, on ACP v2 ([agentclientprotocol#1346](agentclientprotocol#1346)) ([d528647](agentclientprotocol@d528647)) * show nothing of a tool that cancelled work asks to run after the cancel was reported ([agentclientprotocol#1334](agentclientprotocol#1334)) ([ebf8b49](agentclientprotocol@ebf8b49)) * stop waiting for a question the client leaves open after Claude Code cancels it ([agentclientprotocol#1338](agentclientprotocol#1338)) ([c1e83b4](agentclientprotocol@c1e83b4)) * update Claude Agent SDK to 0.3.300 ([agentclientprotocol#1355](agentclientprotocol#1355)) ([761b4c8](agentclientprotocol@761b4c8)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: acp-release-bot[bot] <246668977+acp-release-bot[bot]@users.noreply.github.com>
…ntprotocol#1357) ## Summary `agentFileChangeReport` took its path list only from Claude's file checkpoints (`rewindFiles` dry run). A checkpoint tracks only the files that the root agent edits with its own file tool. So a file that only a subagent edited never appeared in the report. Air then showed no "Since last turn" row for it, and Last Turn review could not include it. - Collect the `file_path` / `notebook_path` of successful `Edit`/`Write`/`MultiEdit`/`NotebookEdit` results from subagent messages in the stream. Store them on the active turn. This also covers background subagents, because the turn stays open until they finish. - Merge these paths with the checkpoint list. When the checkpoint preview times out or is unavailable, report the subagent paths instead of `unavailable`.
…ns (agentclientprotocol#1356) ## What The Claude Code CLI can predict the prompt the user is likely to send next. This adapter asked for nothing and dropped the `prompt_suggestion` message. It now asks for the prediction (`promptSuggestions: true`) and forwards the one prediction of a turn. ACP has no field for a composer prefill, so the value travels as ```json { "sessionUpdate": "session_info_update", "_meta": { "promptSuggestions": { "suggestions": [{ "text": "Run the tests" }] } } } ``` The key names no vendor, so another client can read it. AIR already renders the first non-blank text as gray text in an empty composer, where Tab fills the composer and sends nothing. ## Behavior - `suggestions: []` clears the prediction the client holds: at the next `session/prompt`, because that turn's prediction is spent, and on a `conversation_reset`, because the transcript it was drawn from is gone. - A prediction the next prompt outran — the user sent the next prompt before the adapter read the prediction off the SDK stream — is dropped instead of published mid-turn. - A blank prediction never goes on the wire. - The update is sent only to an AIR client, like the other extension keys, so non-AIR traffic is unchanged (the key is added to `AIR_ONLY_META_KEYS`; the `origin/main` scenario baselines stay byte-identical). - The CLI predicts nothing on the first turn, after an API error, in plan mode, and at the usage limit, so a turn without a prediction is normal. A caller can opt out with `_meta.claudeCode.options.promptSuggestions: false`. Documented in `docs/air-extensions.md`. ## Tests `src/tests/prompt-suggestions.test.ts` covers the wire shape, the publish after the turn's result, the clear at the next prompt, the dropped stale prediction, the blank prediction, and a non-AIR client. `src/tests/create-session-options.test.ts` covers the SDK option and the caller opt-out. `npm run check`, `npm run build` and the touched suites are green; the full `test:run` has pre-existing flakiness on this machine (session-index watchers, providers) unrelated to this change. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-authored-by: Claude <[email protected]> Co-authored-by: Happy <[email protected]>
…col#1359) AIR gets `_meta.jetbrains.air.mcp = {server, tool}` on MCP tool calls, so the MCP card does not depend on the title format. The adapter splits the `mcp__<server>__<tool>` tool name the way Claude Code does. `server` is the normalized name. Clients other than AIR get nothing new. Split from IJAI-872. The SDK does not expose MCP progress yet, so this PR does not forward it.
…rotocol#1364) After a restart the AIR subagent map is empty, so a SendMessage resume opened no generation and the child's messages stayed buffered under the old launch id. Restore the agent from the session history on a resume signal or on a message with an unseen parent_tool_use_id, with a generation id that can't collide with ids the client stored before.
…gentclientprotocol#1366) Updates `@agentclientprotocol/sdk` to 1.9.0 (schema v1.26.0, v2.0.0-alpha.9) and adapts the agent to the v2 spec clarifications in that release. ## Changes **Client-provided system prompt (unstable)** - Advertises `sessionCapabilities.systemPrompt` with `mode.override` on v1 and `session.systemPrompt` on v2. - Accepted on `session/new`, `load`, `resume`, and `fork`: - `append` adds the text after the Claude Code preset. - `override` replaces the preset with the text. Tools, MCP servers, permissions, and settings (including CLAUDE.md through `settingSources`) are unchanged. - An unsupported mode or non-text content fails with `invalid_params` before any session is created, reused, or forked. - Takes precedence over the `_meta.systemPrompt` extension. AIR custom instructions still follow the client text. - A different prompt on resume recreates a live session. Claude Code reuses its saved system prompt until the next compaction, which the RFD allows. **v2 session/resume state snapshot** - Sends the current foreground state (`idle`, `running`, or `requires_action`) after any replay and before the response, as the clarified spec requires. This applies even without a replay. **v2 replay of subagents** - No longer replays a child's past `running`/`idle` as current activity. Each replayed child gets one `state_update` with its last state after the replay. **v2 tool call `_meta` patches** - Reports carry only the top-level keys that changed. `claudeCode` is still sent whole, since clients don't merge nested objects. - A removed key is sent as `null`. - A tool call id that starts again removes the old keys. - A replayed tool call this connection has not reported sends `_meta: null` first, then the current keys. **Cancellation and close** - A `session/close` whose turns don't end within the teardown timeout settles them as cancelled. The client gets `idle` with `cancelled`, or `-32800` for a prompt never inserted, before the close response. - A prompt racing `session/close` ends as cancelled (`-32800` on v2) instead of `-32603`. On v1 it now returns `stopReason: cancelled`, matching a prompt queued just before the close. - A stream error after a cancel ends the work as `cancelled` instead of `error`. ## Not in this PR - After a URL-mode MCP elicitation is accepted, the session reports `running` right away. The spec asks for `requires_action` until the external interaction finishes, but a server may never send `elicitation_complete`, so this needs its own design. - `requires_action` is reported while one foreground subagent waits, even if other foreground work can continue. This is SHOULD-level in the spec. ## Testing - `npm run build`, `npm run check`, and `npm run test:run` pass. - Golden files changed because of the new capability and the resume snapshot. `compare.ts` allows the new capability against origin/main. - `session index cost > reads only the changed transcript for a subscription` failed intermittently locally (about 3 in 15 runs) on a loaded machine. This PR doesn't touch the session index, but watch it in CI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <[email protected]>
🤖 I have created a release *beep* *boop* --- ## [0.92.0](agentclientprotocol/claude-agent-acp@v0.91.0...v0.92.0) (2026-10-16) ### Features * add the MCP server and tool to AIR tool calls ([agentclientprotocol#1359](agentclientprotocol#1359)) ([872e93f](agentclientprotocol@872e93f)) * publish the predicted follow-up prompt as _meta.promptSuggestions ([agentclientprotocol#1356](agentclientprotocol#1356)) ([78a29f5](agentclientprotocol@78a29f5)) * update ACP SDK to 1.9.0 and adopt the v2 spec clarifications ([agentclientprotocol#1366](agentclientprotocol#1366)) ([68bf74f](agentclientprotocol@68bf74f)) ### Bug Fixes * include subagent file edits in the file-change report ([agentclientprotocol#1357](agentclientprotocol#1357)) ([910d83c](agentclientprotocol@910d83c)) * restore resumed subagents after an adapter restart ([agentclientprotocol#1364](agentclientprotocol#1364)) ([26f144a](agentclientprotocol@26f144a)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: acp-release-bot[bot] <246668977+acp-release-bot[bot]@users.noreply.github.com>
…sponse is out (agentclientprotocol#1368) ## Problem `session/fork` copies the transcript and then starts the forked session, which takes several seconds. Meanwhile the list subscription sees the new transcript and sends `_session/list/changes` with the fork row before the fork response. The client meets an unknown session and lists it as a session of its own. JetBrains Air then refuses the fork (IJAI-4105). ## Fix - `ForkHolds` (in `session-index/list-subscriptions.ts`, owned by the session index service) holds the row of a fork until the `session/fork` response is out: - by `forkedFrom`, for an unsent row, while the transcript is copied; - by the fork id after the copy. This covers a fork whose first record is over 64 KB, because the scan reads `forkedFrom` from the head only. - `session/list` leaves a held fork out too. - The v1 and v2 fork handlers run in `forking(...)`. A `setTimeout(0)` releases the rows after the SDK has written the response. - The holds live in the service, so a subscription that starts during a fork holds that fork too. ## Tests `holds a fork back until the response of its session/fork is out` (session-index.test.ts). It fails without the fix.
…on (agentclientprotocol#1197) Adds a `/remote-control [name]` command (alias `/rc`) so a session hosted over ACP can be continued from claude.ai/code or the Claude mobile app. Addresses agentclientprotocol#459. The CLI's own `/remote-control` is terminal-bound and isn't advertised to SDK hosts, and `remoteControlAtStartup` only applies to interactive sessions. The SDK does expose a runtime `enableRemoteControl(enabled, name?)` on the query object, which returns the session URL. See anthropics/claude-agent-sdk-typescript#460. - The adapter handles the command itself in `prompt()`, calls `enableRemoteControl(true, name)`, and replies with the session URL as an agent message. The turn ends without going to the model. - Failures (for example Remote Control disabled by policy) are reported to the user in the same way. - The command is added to `available_commands_update` only when the query exposes the method. - `enableRemoteControl` is not in the published `Query` types yet, so it is typed locally the same way as `mcpAuthenticate`. ## Testing - `npm run check`, `npm run build` and `npm run test:run` pass, including new tests in `src/tests/remote-control.test.ts`. - Ran the built agent through `examples/simple-client.ts` against a real Claude Max session: the command was advertised, `/remote-control` returned a working `claude.ai/code` URL, and prompts sent from the Claude iOS app ran in the session and were answered. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Noah Eisenbruch <[email protected]> Co-authored-by: Andrey Bragin <[email protected]>
🤖 I have created a release *beep* *boop* --- ## [0.93.0](agentclientprotocol/claude-agent-acp@v0.92.0...v0.93.0) (2026-10-16) ### Features * add /remote-control command to start Remote Control for a session ([agentclientprotocol#1197](agentclientprotocol#1197)) ([db9f50f](agentclientprotocol@db9f50f)) ### Bug Fixes * hold a fork back from the session list until its session/fork response is out ([agentclientprotocol#1368](agentclientprotocol#1368)) ([f5967fd](agentclientprotocol@f5967fd)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: acp-release-bot[bot] <246668977+acp-release-bot[bot]@users.noreply.github.com>
Conflicts resolved by keeping both sides: - src/acp-agent.ts: the account-limits reader next to upstream's gateway model listing; the _session/state_changed notification after upstream's session-index bookkeeping; withRunningTerminal around the AIR-only background marker. - src/native-subagents.ts: subagent type and model metadata next to the RFD labels. Upstream's NativeSubagentRuntime now takes a SubagentForm, so the metadata tests pass "air". The v2 golden files gain the fork's nativeSubagent session_info_update. Upstream-Sync-Base: c8a5653 Upstream-Sync-Release: dbf386d
OldKrab
marked this pull request as ready for review
October 17, 2026 17:53
This was referenced Oct 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merges the upstream v0.93.0 release (from v0.88.1-preview.1, 63 upstream commits) and updates
@anthropic-ai/claude-agent-sdkfrom upstream's 0.3.300 to 0.3.302 (Claude Code 2.1.302). Supersedes #15, #18 and #19.Upstream brings ACP SDK 1.9.0 with the v2 spec clarifications, subagent cancel/restore fixes, the session index, gateway model listing,
/remote-control, and prompt suggestions.Conflicts, resolved by keeping both sides:
src/acp-agent.ts: the account-limits reader next to upstream's gateway model listing; the_session/state_changednotification after upstream's session-index bookkeeping;withRunningTerminalaround the AIR-only background marker.src/native-subagents.ts: subagent type/model metadata next to the RFD labels.package.jsonand the lockfile were merged byscripts/prepare-upstream-sync.mjs.Follow-ups from the merge: upstream's
NativeSubagentRuntimenow takes aSubagentForm, so the metadata tests pass"air"; the six new v2 golden files gain the fork'snativeSubagentsession_info_update(additive).SDK 0.3.301–0.3.302 behaviour changes checked: the adapter always passes
permissionModeexplicitly, so the new third-party default does not apply; fork subagents arrive as Agent tool calls.Local:
npm run build,npm run check,npm run test:run(3182 passed, 32 skipped).