mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 04:30:59 +00:00
main
890 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
03d84f6663 |
feat: add the AIR sessionIndex extension for fast session lists (#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 <noreply@anthropic.com>
v0.88.1-preview.7
|
||
|
|
15034659f7 |
refactor: list a session's commands in its setup responses on the experimental ACP v2 surface (#1290)
The v2 draft lets the `session/new`, `session/resume`, and `session/fork` responses carry `availableCommands` ([agent-client-protocol#2259](https://github.com/agentclientprotocol/agent-client-protocol/pull/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 (#1253, #1255, #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.v0.88.1-preview.6 |
||
|
|
55c3106c60 |
fix: report an interrupted tool call as cancelled on the experimental ACP v2 surface (#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`.
v0.88.1-preview.5
|
||
|
|
060bdb212a |
fix: report a failed turn with the error stop reason on the experimental ACP v2 surface (#1288)
Follow-up to #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](https://github.com/agentclientprotocol/agent-client-protocol/pull/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.v0.88.1-preview.4 |
||
|
|
446a7217fa |
feat(deps): Bump @agentclientprotocol/sdk to 1.8.0 (#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](https://github.com/agentclientprotocol/agent-client-protocol/pull/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 (#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.v0.88.1-preview.3 |
||
|
|
908c994901 | feat: Support custom instructions for agent sessions via meta (#1177) v0.88.1-preview.2 | ||
|
|
966be7abe6 |
fix: recreate live sessions on logout when it clears a provider or gateway override (#1278)
## What ACP `logout` cleared `providerConfig` and the gateway credentials but left live queries running. Provider and gateway routing is baked into each query's env, so open sessions kept using the old route after logout. `logout` now recreates every live session through the same path as `providers/disable` (`enqueueProviderUpdate`), but only when a provider or gateway override was active. It waits for active turns, then resumes each session under its id with native routing. This also runs when `claude auth logout` fails, because the override is already cleared by then. A plain CLI-login logout does not touch live sessions: the running CLI picks up the logout by itself. ## Behavior change With an override active, `logout` now waits for running turns before it returns, like `providers/disable`. Under `--hide-claude-auth`, a session that cannot be recreated signed out is dropped, as with `providers/disable`.v0.88.1-preview.1 |
||
|
|
d43fec3fa3 |
chore(main): release 0.88.0 (#1277)
🤖 I have created a release *beep* *boop* --- ## [0.88.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.87.0...v0.88.0) (2026-10-08) ### Features * **deps:** update Claude SDK ([#1276](https://github.com/agentclientprotocol/claude-agent-acp/issues/1276)) ([v0.88.0 |
||
|
|
0304f743f8 | feat(deps): update Claude SDK (#1276) v0.87.1-preview.2 | ||
|
|
6a162baf8b |
feat(deps): Bump @anthropic-ai/claude-agent-sdk to 0.3.293 (#1271)
Bumps the minor group with 12 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@anthropic-ai/claude-agent-sdk](https://github.com/anthropics/claude-agent-sdk-typescript) | `0.3.287` | `0.3.289` | | [eslint](https://github.com/eslint/eslint) | `10.11.0` | `10.12.0` | | @anthropic-ai/claude-agent-sdk-darwin-arm64 | `0.3.287` | `0.3.289` | | @anthropic-ai/claude-agent-sdk-darwin-x64 | `0.3.287` | `0.3.289` | | @anthropic-ai/claude-agent-sdk-linux-arm64 | `0.3.287` | `0.3.289` | | @anthropic-ai/claude-agent-sdk-linux-arm64-musl | `0.3.287` | `0.3.289` | | @anthropic-ai/claude-agent-sdk-linux-x64 | `0.3.287` | `0.3.289` | | @anthropic-ai/claude-agent-sdk-linux-x64-musl | `0.3.287` | `0.3.289` | | @anthropic-ai/claude-agent-sdk-win32-arm64 | `0.3.287` | `0.3.289` | | @anthropic-ai/claude-agent-sdk-win32-x64 | `0.3.287` | `0.3.289` | | [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk) | `1.31.0` | `1.32.1` | | [hono](https://github.com/honojs/hono) | `4.13.12` | `4.13.13` | Updates `@anthropic-ai/claude-agent-sdk` from 0.3.287 to 0.3.289 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/anthropics/claude-agent-sdk-typescript/releases">@anthropic-ai/claude-agent-sdk's releases</a>.</em></p> <blockquote> <h2>v0.3.289</h2> <h2>What's changed</h2> <ul> <li>Updated to parity with Claude Code v2.1.289</li> </ul> <h2>Update</h2> <pre lang="sh"><code>npm install @anthropic-ai/claude-agent-sdk@0.3.289 # or yarn add @anthropic-ai/claude-agent-sdk@0.3.289 # or pnpm add @anthropic-ai/claude-agent-sdk@0.3.289 # or bun add @anthropic-ai/claude-agent-sdk@0.3.289 </code></pre> <h2>v0.3.288</h2> <h2>What's changed</h2> <ul> <li>Updated to parity with Claude Code v2.1.288</li> </ul> <h2>Update</h2> <pre lang="sh"><code>npm install @anthropic-ai/claude-agent-sdk@0.3.288 # or yarn add @anthropic-ai/claude-agent-sdk@0.3.288 # or pnpm add @anthropic-ai/claude-agent-sdk@0.3.288 # or bun add @anthropic-ai/claude-agent-sdk@0.3.288 </code></pre> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/anthropics/claude-agent-sdk-typescript/blob/main/CHANGELOG.md">@anthropic-ai/claude-agent-sdk's changelog</a>.</em></p> <blockquote> <h2>0.3.289</h2> <ul> <li>Updated to parity with Claude Code v2.1.289</li> </ul> <h2>0.3.288</h2> <ul> <li>Updated to parity with Claude Code v2.1.288</li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="v0.87.1-preview.1 |
||
|
|
b2dbc8f5a1 |
chore(main): release 0.87.0 (#1261)
🤖 I have created a release *beep* *boop* --- ## [0.87.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.86.0...v0.87.0) (2026-10-07) ### Features * **deps:** Bump the minor group with 2 updates ([#1260](https://github.com/agentclientprotocol/claude-agent-acp/issues/1260)) ([v0.87.0 |
||
|
|
390b0b9f8b |
fix: settle prompts folded into a task-notification cycle (#1233)
## Motivation A prompt sent while Claude Code runs a turn it started on its own, for example after a background task finished, is folded into that turn. The turn's result keeps `origin: task-notification` but lists the prompt in `user_message_uuids`. The adapter routed results by origin only, so `session/prompt` was never answered and the client stayed in a running state. A JetBrains AIR session stayed "running" for hours this way. ## Change A task-notification result that names a pending prompt now answers it. A turn held for its subagents still waits for them. Results without the uuids behave as before. ## Tests Unit tests in `acp-agent.test.ts` and a replay of the session in `folded-prompt-scenario.test.ts`. Both hang without the fix.v0.86.1-preview.3 |
||
|
|
0724b17c58 |
feat(deps): Bump the minor group with 2 updates (#1260)
Bumps the minor group with 2 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [ip-address](https://github.com/beaugunderson/ip-address). Updates `@types/node` from 26.6.3 to 26.6.4 <details> <summary>Commits</summary> <ul> <li>See full diff in <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare view</a></li> </ul> </details> <br /> Updates `ip-address` from 10.7.2 to 10.7.3 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/beaugunderson/ip-address/releases">ip-address's releases</a>.</em></p> <blockquote> <h2>v10.7.3</h2> <h2>What's Changed</h2> <ul> <li>Reject an in-addr.arpa name longer than 32 characters before splitting it by <a href="https://github.com/beaugunderson"><code>@beaugunderson</code></a> in <a href="https://redirect.github.com/beaugunderson/ip-address/pull/228">beaugunderson/ip-address#228</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/beaugunderson/ip-address/compare/v10.7.2...v10.7.3">https://github.com/beaugunderson/ip-address/compare/v10.7.2...v10.7.3</a></p> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="v0.86.1-preview.2 |
||
|
|
0e83a06c4a |
fix: async task routing, Monitor tasks, structured task ids, and abandoned tool calls (#1249)
It affects AIR clients with the `asyncTasks` capability.
## Problem
- A subagent can start a background Bash or Monitor and then end its
turn. The task ends after the subagent finished. The route of the task
dropped every update of a finished child generation, so the client never
got the terminal `async_task_state_update`. The task stayed running in
the client forever. One AIR log held 440 such dropped updates.
- The `Monitor` tool streams the stdout of a background command to the
model only. The SDK reports its task as `local_bash`, so the client
showed it as a background shell. In one real session, 18 of 18 Monitor
tasks had no output file, no progress and no result. The Stop button
only broke the agent's wait.
- The adapter read a task id from the text of a Bash tool result. A
foreground `sed`, `strings` or `python3` that printed adapter source,
test fixtures or CLI strings became a background task, for example with
the id `";\n const start = text` or `${e}`. The client got a spawn and a
`backgrounded` marker on a foreground call, and the task never ended.
- Claude Code 2.1.287 reports a background command in four texts, and
only one gave an output path. A subagent tool result almost never has
`tool_use_result` (0 of 48 in one session). So a subagent command that
hit its timeout got no output path (9 of 10).
## Fix
- `NativeSubagentRuntime.routeOfToolCall` still sends
`async_task_spawned`, `async_task_progress` and
`async_task_state_update` to the child generation that owned the tool
call after that child finished. It never sends them to a later
generation. Every other late update is dropped as before. The spawn is
in the list because a held task can get its tool call id after the
subagent finished.
- `AsyncTaskRuntime` takes a `toolNameOf` lookup. A task of a `Monitor`
tool call is ignored, like a subagent task: no spawn, no progress, no
state, and no stop. A held task that gets its tool call id later goes
through the same check. A `local_monitor` task is ignored too. The
`Monitor` tool call stays in the transcript.
- A task comes only from structured data:
`tool_use_result.backgroundTaskId` or the SDK task events. The text id
parser is gone.
- The output path of a known task can come from the result of its own
tool call. The path ends with `tasks/<id>.output` and starts after the
nearest `": "` on the same line, so a path with spaces works, for
example under `C:\Users\John Doe\AppData\Local\Temp`. It must be
absolute (POSIX, a Windows drive or UNC), and it never holds the end of
another path. The parse is linear in the text length. All four texts
give the path in both event orders: the tool result before or after the
SDK names the task. The memory is bounded. A structured path still wins,
and the terminal `output_file` reaches the client also after a level
close.
- The `backgrounded` marker comes only from structured data: the
structured task, a known background task of the tool call, or a
`run_in_background` input. The "both event orders" above holds for the
path, not for this marker. The marker rides the tool result update, so a
command that the SDK names only after its tool result gets the marker
only from a `run_in_background` input. A command that the user moved to
the background by hand gets no marker in that order.
- The replay behaves the same in each case.
## Tests
- Native subagent and agent tests: a task update and a late spawn after
the child finished go to `worker-1`; a held task of a subagent gets its
spawn and state in the child session after the subagent finished; a late
message chunk is still dropped; a subagent's background Bash ends after
the subagent and the client gets the terminal state in the child
session.
- Monitor: runtime, live and replay tests, also a task that names its
tool call after the tool result. A Monitor task cannot be stopped.
- Structured ids: foreground output with each marker text creates no
task; each of the four texts gives the path in both orders, also a
Windows and a POSIX path with spaces and a UNC path; no path of another
task, another tool call, `.outputs`, a relative path, a path without a
`": "` boundary, or a path across a line; two paths on one line are not
glued; a 2 MB text without `": "` parses in milliseconds; a structured
path wins. An agent test covers a subagent without `tool_use_result`,
and a replay test takes the path from the tool result.
- The new tests fail without the change. Build, lint, format and all
tests pass.
## Abandoned streamed tool calls
This part affects every client, Zed included.
**Problem.** The adapter opens a tool call at the `content_block_start`
of a streamed `tool_use`. Claude can abandon that block before it is
complete, for example for a steering message. The tool then never runs.
At end_turn the settle check reported the session failure "Claude ended
the turn without returning results for tool calls"
(`incomplete_tool_call`). A real AIR session showed a red service error
after 27 minutes of normal work, for a call that never had input.
**Fix.**
- The session records the tool calls that Claude Code sent to the tool
runner: the `tool_use` blocks of a complete assistant message, and the
tool calls of a permission request. The record is bounded and is dropped
when the call ends.
- At end_turn, an open foreground call without that record is abandoned.
The adapter closes it as failed with "Claude stopped this tool call
before it ran." and reports no session failure. The turn ends as
`end_turn`.
- An open call with that record keeps the old behaviour: the old failure
text and the `incomplete_tool_call` session failure.
- The cleanup and the cancellation checks are the same for both groups.
- The replay does not change, because the history holds no partial
`tool_use`. No scenario golden file changes, for AIR, plain or Zed.
**Tests.** A partial `tool_use` and then text ends as `end_turn` with
the neutral close. A mixed turn still fails for the unfinished call. A
steering message during a partial `tool_use` gives no failure. The
existing incomplete-tool tests now send the complete `tool_use` and keep
their failure. The new tests fail without the change.
v0.86.1-preview.1
|
||
|
|
7b5c61a4ed |
chore(main): release 0.86.0 (#1225)
🤖 I have created a release *beep* *boop* --- ## [0.86.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.85.1...v0.86.0) (2026-10-05) ### Features * answer /mcp in chat with the MCP server status and a reconnect ([#1218](https://github.com/agentclientprotocol/claude-agent-acp/issues/1218)) ([v0.86.0 |
||
|
|
5598efbc8a | fix: excessive message/token_count API calls when probing context window (#1258) v0.85.2-preview.26 | ||
|
|
07c7ed3752 |
refactor: fork sessions on the experimental ACP v2 surface (#1256)
Step 6 of opt-in ACP v2 support, option (b) as decided in review. Stacked on #1255: both change the `initialize` line of every v2 golden file. Once #1255 merges, I'll merge `main` into this branch so the diff shows fork alone. Nothing changes on ACP v1. ## Why fork, then resume The agent's v1 fork copies the transcript into a new session and returns only its id. Until a client resumes or loads the fork, a prompt to it fails with "Session not found", and the request's `mcpServers` go unused. AIR, the only v1 client that forks, loads the fork right away. The fork RFD describes the response as "the same as `session/new`", with config options, and the request as taking `session/load`'s options. So on v2 the client gets a session it can prompt at once: - `v1ForkSessionRequests` (`src/v2/session.ts`) builds the v1 fork, then a v1 resume of the fork without replay, with the request's `cwd`, `additionalDirectories` and `mcpServers`. The request's `_meta` goes to the fork alone. - The response is the new `sessionId` plus the resume's `configOptions`. The available commands follow as an update, as for `session/new` and `session/resume`. - `capabilities.session.fork: {}` is advertised. Each fork starts its own Claude Code process, as `session/new` does. The draft doesn't say whether a fork must be live, so that's recorded as feedback for it. ## Verification - An end-to-end test forks a session with an HTTP MCP server, checks the forked Claude Code session resumes the copied transcript with that server, and prompts the fork without a `session/resume`. - Removing the resume, the MCP servers or the capability fails tests. - Every v2 golden file changes on its `initialize` line only, which gains `session.fork`. - Build, lint and all 2411 tests pass.v0.85.2-preview.25 |
||
|
|
3a61172e19 |
refactor: serve providers on the experimental ACP v2 surface (#1255)
Step 6 of opt-in ACP v2 support. Stacked on #1254: both change the `initialize` line of every v2 golden file. GitHub retargets this PR to `main` once #1254 merges. Nothing changes on ACP v1. ## Change - The v2 surface serves `providers/list`, `providers/set` and `providers/disable` through the agent's unstable v1 methods. Every provider type is identical in v1 and v2, so requests and responses pass through unchanged. - The v2 `initialize` response advertises `capabilities.providers: {}`, a constant like `capabilities.session`, since the agent serves the methods for every client. ## Verification - An end-to-end test lists the provider, sets a gateway and lists it, gets `invalid_params` for an unknown provider, and disables the provider back to the native routing. - The handshake test checks the capability. Removing the capability or a handler fails tests. - Every v2 golden file changes on its `initialize` line only, which gains `providers: {}`. - Build, lint and all 2410 tests pass.v0.85.2-preview.24 |
||
|
|
4ddda8f37e |
refactor: stop advertising steering on the experimental ACP v2 surface (#1254)
Found while reviewing what's left of opt-in ACP v2 support. Nothing
changes on ACP v1.
## Problem
The agent advertises `_session/steering` in the top-level `_meta` of its
v1 `initialize` response (`steering: { supported: true }`), and the v2
handshake passed that `_meta` through. The v2 surface doesn't register
`_session/steering`, so a v2 client that steered got "method not found".
## Change
The v2 `initialize` response leaves out the top-level `_meta` keys that
advertise an extension the v2 surface doesn't serve
(`UNSERVED_EXTENSION_KEYS` in `src/v2/initialize.ts`), which is
`steering` alone today. That sits next to the existing rule that keeps
the unstable `providers` capability unadvertised.
Whether v2 serves steering is left to the plan for the unstable and
extension surfaces. The protocol has no steering RFD yet, and steering
may need a different design on v2, which answers a prompt as soon as
it's accepted.
## Verification
- The handshake test checks that the v2 response has no
`_meta.steering`. The v1 handshake tests still check that v1 advertises
it.
- Every v2 golden file changes on its `initialize` line only. The line
loses its `_meta` and is otherwise identical.
- Build, lint and all 2409 tests pass.
v0.85.2-preview.23
|
||
|
|
96acfeccf9 |
refactor: report compaction and cover notices on the experimental ACP v2 surface (#1253)
Part of step 6 of opt-in ACP v2 support. Compaction and session notices are both Preview features of the v2 draft that are expected to stabilize soon, and v2 requires no client capability for either. Nothing changes on ACP v1. ## Compaction The agent already implements the compaction RFD for v1 clients that declare `clientCapabilities.session.compaction`, live and on replay. A v2 client got the legacy "Compact conversation" tool call instead, because the v2 surface didn't declare it. - `v1InitializeRequest` now tells the agent that every v2 client takes compaction updates, as it already did for notices. - `v2SessionUpdate` passes `compaction_update` and `compaction_summary_chunk` through: their v1 and v2 shapes are identical. So a v2 client now gets: - **live:** `compaction_update` `in_progress`, the summary as `compaction_summary_chunk`, then `completed` (or `failed` with the standard `error`); - **on replay:** one completed `compaction_update` with the summary, at the summary's position in the transcript. That's the materialized form the v2 draft asks for, without the start/chunk/finish sequence. The token counts and duration that the tool call's `rawOutput` carried have no v2 field. Only AIR gets them, in `_meta`. ## Notices These were already sent to every v2 client, but no test covered one on v2. A new end-to-end test sends Claude Code's `informational` warning as a live `notice`. The agent sends notices only live, never during replay, which matches the draft's "SHOULD NOT be replayed". ## Not changed **A replayed compaction's id differs from its live id.** Live, the id is the uuid of Claude Code's `compacting` status, which the transcript doesn't keep. Replay uses the summary message's uuid. The draft and the RFD ask for the original, stable ids. As with replayed message ids, a client that kept its view would see the compaction twice. Matching them needs Claude Code to persist the live id. ## Verification - The 3 compaction scenarios change their v2 golden files: the tool call becomes the compaction entity. The scenario names still say `legacy` because they're named for the v1 capability. - Unit tests cover the pass-through and the capability. End-to-end tests cover the live notice, and the replayed compaction between the messages around it. - Removing the capability or the pass-through fails tests. - Build, lint and all 2409 tests pass.v0.85.2-preview.22 |
||
|
|
52a92be959 |
fix: report a command's exit code only when the tool result says it (#1252)
Found while mapping commands to ACP v2 display terminals (#1240). It affects Zed and plain ACP v1 terminal clients and the v2 surface. **AIR is unchanged.** ## Problem The agent reported every command's exit with a number, even when it didn't know the code: - **any failure as 1.** A command that exited with 2, or a denied command that never ran, showed "Exited with code 1". - **a backgrounded command as 0 when it launched**, though it's still running. - **an interrupted command as 1**, and a non-zero code that Claude Code accepted as a success (grep with no matches) as 0. ## Change `CommandOutput.exitCode` is now optional. `BashReporter` sets it only when the tool result says it. I checked each source against the Claude Code 2.1.287 binary: - **a failure:** the `Exit code N` line that starts Claude Code's text of a command that exited with a failing code (`rE`/`JPr` for a `ShellError`). A failure that names none, such as a denial or a spawn error, has no code. - **a success:** 0, unless the structured `BashOutput` says otherwise. Claude Code accepts a non-zero code as a success only with a `returnCodeInterpretation`, such as grep's "No matches found" or diff's "Files differ". An interrupted command (an Esc interrupt returns a success with `interrupted: true`) and a backgrounded one have no code either. - **the code-execution block:** its `return_code`, as before. An unknown code goes out as: - **v1, Zed and plain clients:** `_meta.terminal_exit.exit_code: null`. The v1 `TerminalExitStatus` allows null. Zed (`as_u64()` → `None`) and Delta's v1 adapter (`Option<u32>`) read it as an optional code. Zed then shows a failed command as failed, without the "Exited with code N" tooltip, and every other exit as before. `compare.ts` allows the null code against the origin/main baseline, a documented difference like the `mcp` command in #1218. - **v2:** `exitStatus` without `exitCode`, which the v2 schema treats the same as null. - **AIR:** the numbers it always got (1 for a failed or interrupted command, else the code, or 0). Whether AIR accepts a null code is unchecked, so this stays out of AIR's contract. The command fact carries `interrupted` for that. The AIR golden files and `docs/air-extensions.md` don't change. ## For review - **Interrupted commands** no longer show as failed in Zed, since the tool call completes and no code is known. Their output still ends with "[Command was aborted before completion]". ## Not changed - **A backgrounded command's terminal still exits at launch**, now without a code. It could stay running until the task's `task_notification`. That needs the task's end routed to the terminal, also on replay, so it's a separate change. - **Replay has no `tool_use_result`** (the same gap as replayed diffs), so a replayed success still reports 0 when Claude Code interpreted the code or backgrounded the command. ## Verification A table test pins, for each kind of exit, the code Zed gets and the number AIR gets: a success, a failure that names its code, one that names none, an interrupted, a backgrounded, and an interpreted command. A unit test covers the v2 `exitStatus` without a code. Removing any one of the rules, including AIR's, fails tests. Golden files change for 3 v2 scenarios, each on its exit line only. Build, lint and all 2399 tests pass.v0.85.2-preview.21 |
||
|
|
8589a9cf15 |
fix: restore main-thread agent config option (#1250)
## Why #1112 removed the main-thread agent picker on the assumption that ACP clients don't use it. That was wrong: Zed exposes the `Agent` config option and users rely on it daily to start sessions with a custom agent (see the feedback on #1112). It is also still the only way to pick a main-thread agent for enterprise Claude Code setups. Restoring it has no meaningful cost: `supportedAgents()` reads from the already-awaited initialization response, so session creation doesn't gain a round-trip. ## Summary Reverts #1112: - restore the `agent` session config option (shown only when custom agents are configured) and custom-agent discovery - restore live agent switching via `applyFlagSettings({ agent })` - forward `_meta.claudeCode.options.agent` again and keep it across context resets - restore the exports `DEFAULT_AGENT_ID`, `AGENT_CONFIG_ID`, `BUILTIN_AGENT_NAMES`, `discoverCustomAgents` Conflicts with later changes on `main` (MCP command import, initial permission mode / bypass resolution, context-window refresh after model switch, effort settings) were resolved keeping the newer code. ## Testing - npm run check - npm run build - npm run test:run (2399 passed) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>v0.85.2-preview.20 |
||
|
|
a44c486019 |
fix: end a cancelled turn when the force-cancel fires during an update (#1243)
Found while writing the test for #1242. It affects ACP v1 and v2. ## Problem When Claude Code wedges after a cancel (issue #680), the force-cancel backstop aborts the consumer's cancel signal, so the consumer settles the turn as `cancelled`. The consumer races each next message against that signal by adding an `abort` listener on each iteration. But a listener added to an already-aborted signal never fires. So if the backstop fired while the consumer was busy outside the race, such as sending an update to a slow client, the consumer missed it. It then waited for a message from the wedged Claude Code that never came, and the cancelled `session/prompt` was never answered (v2: no `idle cancelled`). Teardown aborts the same signal, which is how #1242's test ran into it. ## Change The race also wakes for a signal that's already aborted: - The consumer re-arms its signal after each abort it handles, so an aborted signal there is always an unhandled abort. It can't loop. - A message that's already waiting still wins the race, since `Promise.race` checks it first, so no message is dropped. ## Verification A new turn-events test has Claude Code stream an answer and then wedge. The client takes the chunk slowly, the user cancels, and the backstop (10 ms in the test) fires while the consumer is still sending. The turn ends `cancelled`; without the change it never ends. Build, lint and all 2389 tests pass. |
||
|
|
d599333b72 |
fix: count a subagent's permission requests as the turn waiting on the user (#1241)
Found while planning the permission requests of opt-in ACP v2 support (step 5 of the v2 plan). It affects ACP v1 clients that show native subagent sessions (`subagents` capability, or AIR's native subagent sessions). ## Problem For such a client, a native subagent's permission request or AskUserQuestion goes to the subagent's child session. The agent counted the open request on that session id, but child sessions aren't in `sessions`, so `withPendingUserInput` counted nothing: - **Steering:** a message steered while the subagent's request was open didn't see it, so it went out with interrupting priority instead of `later`, the priority used while the user has a prompt open. - **Turn state:** the turn didn't report that it awaits the user. That's `requires_action` in ACP v2, though the v2 surface doesn't show child sessions today, so v2 isn't affected yet. ## Change The request is still sent to the child session, but it now counts against the owner session, whose turn and steering it blocks. Both paths take the owner id: `requestPermissionFromClient` already had it (`ownerSessionId`), and `handleAskUserQuestion` gets it next to the session it sends the elicitation to. ## Verification The existing "uses priority:'later' while … is pending" test gains two cases: a subagent's permission request and a subagent's AskUserQuestion, both sent to an announced child session. Each fails when its half of the fix is reverted. Build, lint and all 2390 tests pass.v0.85.2-preview.19 |
||
|
|
b1d6cfc6c9 |
fix: end a closed session's turns before session/close answers (#1242)
Found while reviewing the ACP v2 prompt lifecycle (#1226). It affects ACP v1 and v2. ## Problem `session/close` tears the session down: `teardownSession` cancels its turns, wakes the consumer, and closes the query. It returned without waiting for the cancelled turns to end, so the close could answer first: - **v1:** the `session/prompt` response arrived after the `session/close` response. - **v2:** the turn's `idle cancelled` arrived after the close response. v2's `session/close` cancels "as if `session/cancel` had been called", then frees the session. It happens when the consumer settles the turn late: for example, while it's still sending an update to a slow client, or when it settles the turn only at the end of the stream that the close ends. ## Change `teardownSession` now returns once the turns it cancels have ended (each turn's `completion`), for at most `TEARDOWN_TURN_END_TIMEOUT_MS` (5 s), which it logs if a wedged consumer hits it. Its other callers get the same order too: `session/delete`, the recreation of a session whose parameters changed on resume or load, and `dispose()`. ## Found along the way, not changed here The consumer's abort race adds an `abort` listener to the cancel signal on each iteration. A listener added to an already-aborted signal never fires, so an abort that lands while the consumer is busy, such as sending an update, is missed. In production the close still ends the turn, through the end of the stream, which is the path this PR waits for. But the same gap can defeat the force-cancel backstop for a wedged Claude Code (issue #680). That gets its own PR next. ## Verification A new turn-events test has the client accept an answer chunk slowly, closes the session while the consumer is still sending it, and checks that the turn's end comes before the close returns. It fails without the change: the close returns first. Build, lint and all 2389 tests pass.v0.85.2-preview.18 |
||
|
|
3d9bae941c |
refactor: report commands as display terminals on the experimental ACP v2 surface (#1240)
Third part of step 4 of opt-in ACP v2 support (after #1231, #1232, #1237): a v2 client now sees each command the agent runs in an Agent-owned display terminal, instead of its output as a code block. Nothing changes on ACP v1. ## Why a mapping The agent already reports commands through the terminal extension of ACP v1 clients, as codex-acp does: - `_meta.terminal_info` names a terminal, which the tool call's content references; - `terminal_output` (a snapshot) or `terminal_output_delta` (a chunk) carries the output; - `terminal_exit` carries the exit. v2 standardizes exactly this as display terminals: the same `{ type: "terminal", terminalId }` content, plus `terminal_update` and `terminal_output_chunk` for the terminal's state. So the v2 surface maps the extension rather than adding new agent types. Unlike the agent's private `_meta.claudeCode` keys, these keys are a cross-agent protocol extension, so this maps one protocol form to another. ## Change - `ToolCallClientCapabilities.from` reports a v2 client as taking the extension. - `V2Terminals` (`src/v2/terminal.ts`), one per connection, turns the extension's keys into v2 updates, sent before the tool call report without those keys: - `terminal_info` → `terminal_update` naming the terminal; - `terminal_output` → `terminal_update` with an `output` snapshot, base64-encoded; - `terminal_output_delta` → `terminal_output_chunk`; - `terminal_exit` → `terminal_update` with `exitStatus`. - **The command:** the extension names none, so the terminal gets the command from the tool call's `rawInput` once a report carries it. The first report of a streamed tool use has no input yet. `V2Terminals` remembers each running command's terminal for that, and forgets it at exit or `session/close`. - **A report that carried only the output is dropped.** Its remaining `_meta` (the `parentToolUseId` stamp, for a subagent's command) would replace the tool call's stored `_meta` in v2. - Terminal content passes through the tool call translation. A v2 client has no client terminals, so these are the agent's only terminals. The 8 v2 scenarios that run a command change their golden files: `terminal_update`s in place of the code block, the terminal referenced in the permission request, and the exit before the tool call's status. ## Not changed here The agent reports a backgrounded command as exiting with 0 when it launches, and a failure whose exit code it doesn't know as 1. v1 terminal clients get the same values, since the extension requires a number. v2 can leave the exit status out, or say it's unknown, but that needs the agent's command fact to carry it, and it affects v1 too, so it's left for its own PR. ## Verification - Unit tests cover output chunks and snapshots, signals, the command arriving once, the dropped output report, a report without terminal keys passing untouched, and forgetting a closed session. - Each rule fails a test when removed. - Build, lint and all 2388 tests pass.v0.85.2-preview.17 |
||
|
|
98b17c28c8 |
refactor: advertise sessions on the experimental ACP v2 surface (#1239)
Step 4's last session piece for opt-in ACP v2 support (after #1237, #1238). The v2 surface now serves the whole v2 session baseline, so its `initialize` response advertises `capabilities.session`. Until now a conforming v2 client wouldn't call the session methods. Nothing changes on ACP v1, and the v2 surface stays behind `CLAUDE_AGENT_ACP_EXPERIMENTAL_V2`. ## Change `V2_SESSION_CAPABILITIES` is a constant, since it describes what the v2 surface serves: - **the session baseline:** `session/new`, `list`, `resume` (with replay since #1237), `close`, `prompt`, `cancel`, and `update`; - **`session/delete` and `additionalDirectories`.** Not `session/fork` yet, which v1 advertises but the v2 surface doesn't serve; - **prompt content and MCP transports,** as the agent's v1 `initialize` lists them: images and embedded context; stdio and HTTP servers. v2 has no SSE transport. The top-level `_meta` (`promptQueueing`, `authStatus`) passes through as before. `providers` stays unadvertised. Every v2 golden file changes on its `initialize` line only. ## Release Titled `refactor:` like the other v2 PRs, so it triggers no release on its own. This is the point where the v2 surface becomes usable by a conforming client, so `feat:` would also fit if you want the next release to be a minor one. ## Verification The handshake test pins the advertised capabilities, the schema check validates every recorded `initialize` response, and build, lint and all 2386 tests pass.v0.85.2-preview.16 |
||
|
|
22139f73c4 |
fix: replay a session with a Write on the experimental ACP v2 surface (#1238)
Fixes a bug in #1237 on the opt-in ACP v2 surface. Nothing changes on ACP v1. ## Problem A v2 `session/resume` with `replayFrom` failed with an internal error on any session that contains a Write: - A replay has no approval preview and no PostToolUse hook, so the Write reporter builds a diff from the input for every client on replay. - That's a v1 diff. It even claims a creation, since the input doesn't say whether the file existed. - The v2 surface rejects v1 diffs (#1232), so the replay threw and the resume failed. `session-load-replay` has an Edit but no Write, so the scenario suite didn't catch it. ## Change A v2 client gets no diff from a Write's input, live or replayed: a v2 diff names the operation (`add` or `modify`), which the input doesn't tell. Replayed Writes and Edits show no diff on v2 yet; building them needs the tool's structured result, which the SDK's transcript reader leaves out (see the follow-up discussion). ## Verification A new test replays a transcript with a Write through a v2 `session/resume`: it failed with "Internal error" before the change and passes after. The v1 golden files are unchanged. Build, lint and all 2386 tests pass.v0.85.2-preview.15 |
||
|
|
bd8a161c2e |
refactor: replay session history on the experimental ACP v2 surface (#1237)
Last part of step 4's session baseline for opt-in ACP v2 support (after
#1231–#1235): a v2 `session/resume` can now replay the session's
history. Nothing changes on ACP v1.
## Change
- **Routing.** v2 restores a session with `session/resume` and replays
only when `replayFrom` asks for it. v1 splits that into
`session/resume`, which never replays, and `session/load`, which replays
everything before it responds. That's exactly what v2 requires of
`replayFrom: { type: "start" }`. So `v1RestoreSessionRequest` maps the
v2 request to one of the two. A cursor that starts elsewhere is
rejected, since v1 can only replay from the start.
- **Clearing replayed messages.** The agent replays messages as chunks,
and chunks append. A v2 client may still hold a replayed message, so
before the first replayed chunk of each message, the v2 surface sends
that message with `content: []`. The draft requires that of a message
replayed as chunks. It happens only while that session replays
(`V2ClientConnection.replaying`), and the clear and the chunk go to the
connection together, so nothing comes between them.
- **Prompt ids.** The draft requires a prompt's user message to replay
under the id its `session/prompt` response returned. Since #1226 that id
is the user message's uuid, which Claude Code keeps in the transcript,
so no new code is needed. A test checks it end to end.
## Scenario suite
`session-load-replay` passes, so every scenario now passes on v2. The
status table becomes `V2_STOPS`: the scenarios that stop on v2, which is
empty today and stays as the place for a scenario v2 can't run yet. The
replay golden file shows each message cleared before its chunks, and the
`session/resume` response after the last replayed update.
A replayed Edit or Write shows no diff yet on v2: replay has no
PostToolUse hook. Building it from the transcript is the next PR.
## Verification
- **Replay test:** a live prompt, then resume with replay from a
transcript holding the uuid Claude Code saved. The replayed user message
carries the prompt response's id, each message is cleared before its
chunks, everything arrives before the response, and a live prompt
afterwards gets no clears.
- **Mutations:** each rule fails a test when removed: no clears, clears
that stay on after the replay, a replay served as a plain resume, and
one clear per id rather than per message.
- Build, lint and all 2385 tests pass.
v0.85.2-preview.14
|
||
|
|
63aa3b8428 |
refactor: report plans on the experimental ACP v2 surface (#1235)
Second part of step 5 of opt-in ACP v2 support (after #1234): the v2 surface now sends the agent's plan. Nothing changes on ACP v1. ## Change - v1 `plan` becomes v2 `plan_update` with an items plan: `{ type: "items", planId, entries }`. Plan entries are identical in both versions, and a v2 items plan is replaced whole on each update, as a v1 plan is. The update's `_meta` passes through. - **One plan id per session** (`SESSION_PLAN_ID`). v1 has one plan per session, which every update replaces, whether it comes from TodoWrite or from the Task tools. Several plans, such as one per subagent (a subagent's TodoWrite replaces the session's plan today, marked by `_meta.claudeCode.parentToolUseId`), and v2's unstable markdown plans wait for step 6. ## Scenario suite `todo-write` and `task-create-update` pass, with golden files. That leaves only `session-load-replay` in the status table. ## Verification A unit test covers the mapping; build, lint and all 2383 tests pass.v0.85.2-preview.13 |
||
|
|
b9a6ec00a6 |
refactor: serve permission requests on the experimental ACP v2 surface (#1234)
First part of step 5 of opt-in ACP v2 support (after #1231, #1232): the v2 surface now sends the agent's permission requests. Nothing changes on ACP v1: no v1 golden file changes. ## What differs in v2 A v1 request asks about a tool call, and clients show the tool call's title as the prompt. A v2 request gives the prompt a required `title` and an optional `description` of its own, apart from the tool call. It asks about a `subject`, which can be a tool call or a command. ## Change - **Prompt text.** The agent already computes the prompt's heading and explanation, but only AIR gets them, in `_meta.jetbrains.air.permission`. The agent's request type, `AcpPermissionRequest`, now carries them as `title` and `description` for a v2 client. It's the same rule #1232 uses for diffs: build what v1 can't express only for a v2 client. So v1 requests are byte-identical, and the v1 scenario golden files, which record what the agent hands `requestPermission`, don't change. The v2 surface fails on a request without a title, so a missed site fails loudly. - **Subject.** Always `{ type: "tool_call", toolCall }`, where `toolCall` is the same patch a `tool_call_update` carries (`v2ToolCallPatch`). The `command` subject would suit Bash once display terminals exist, since its `terminalId` refers to one. - **Approval preview.** The exact patch before an Edit or Write runs is now built in v2 form for a v2 client, and the request shows it. This fixed a real gap: the agent pins the preview as content the client holds, but only AIR's request shape included it. Without the fix, a v2 client saw no diff at all, because the hook's identical patch was then dropped as unchanged. - **Answer.** v2's outcomes map back unchanged. An outcome v1 doesn't know, which the draft says is no approval, becomes `cancelled`, and the agent reads that as an aborted tool use. - **`requires_action`** while a request is open was already in place since #1226. ## Scenario suite All 12 scenarios that stopped at a permission request now pass: Bash, Edit (approved and rejected), Write, NotebookEdit, WebFetch, MCP, network, ExitPlanMode (approved and rejected), and two subagent scenarios. Each recording shows `requires_action`, the request, then `running`. Now only replay and the two plan scenarios remain. ## Verification - **End to end:** a new test drives the agent's real `canUseTool` through a v2 client. Allow, reject, and an unknown outcome become allow, deny, and an aborted tool use. - **Unit tests:** the prompt's title is separate from the tool call's (ExitPlanMode: "Ready to code?" vs "Approve Plan"), the preview shows in the request, the v2 preview fallback works, and the title guard rejects a missing title. - **Mutations:** each rule fails a test when removed. Sending `title`/`description` to every client fails 37 tests, including v1 golden files. - Build, lint and all 2380 tests pass.v0.85.2-preview.12 |
||
|
|
c1c8c0377d |
refactor: send structured diffs on the experimental ACP v2 surface (#1232)
Second part of step 4 of opt-in ACP v2 support (after #1231): the v2 surface now sends file changes as v2 diffs. Nothing changes on ACP v1: no v1 golden file changes. ## Why the agent builds them A v2 diff names each change's operation (`add`, `modify`, …) and can carry a `git_patch`. A v1 diff is a path with `oldText`/`newText`, and the agent often fills those with a snippet: an Edit's `old_string`/`new_string`, or one display hunk. So a v1 diff can't be translated. It says neither the operation (`oldText: null` can mean a new file, or a hunk with no old lines) nor where the lines are. So the agent builds v2 diffs itself, at the sites that build v1 diffs and know the change. The v2 surface creates the agent with `{ v2: true }`, which becomes `ToolCallClientCapabilities.v2`. A v1 client can't set it. This follows how the renderer already picks a form by capability, as it does for AIR's `diffPatch`. ## What a v2 client gets - **Live Edit and Write tool calls show no diff,** as a Write already does for AIR. The input holds a snippet, or doesn't say whether the file exists. - **The PostToolUse hook sends the exact patch** built from the written file. That's the existing AIR `diffPatch` path, which now produces either form. When it can't be exact, it falls back to a patch from the toolResponse: - a created file: `add` with its whole text; - an update: `modify` with the display hunks, which are fine for v2's renderable patch; - an update without hunks (previous content too large): the change alone; - an Edit with an empty `old_string`: no diff, since the response looks the same for a created file and a filled empty one. - **`git_patch` headers use absolute paths** without git's `a/` and `b/` prefixes, as the draft's example does: `diff --git /p/a.ts /p/a.ts`. ## Plumbing - The exported `toAcpNotifications` and `streamEventToAcpNotifications` take the agent's tool call capabilities (`toolCallCapabilities`) next to the ACP ones, which can't carry the v2 flag. External callers are unchanged. - The v2 diff travels through the agent as v1-typed content, with one documented cast in `v2DiffContent`. Nothing in the agent reads inside a diff item. - The v2 surface passes v2 diffs through and rejects a v1 diff, so a missed site fails loudly. That's how the scenario suite found the notification functions on the first run. - The approval preview stays AIR-only until v2 serves permission requests (step 5). ## Scenario suite `write-existing` passes, with an exact patch (golden file added). `write-new`, `edit-with-permission` and `edit-rejected` now stop at their permission request (step 5). No scenario stops at a diff any more. ## Verification - Unit tests cover the absolute-path headers, the live tool calls, the exact patch, and each fallback. - Each new rule fails a test when removed: git-style paths in a v2 patch, the v1 snippet for a v2 Edit, the AIR path for a v2 hook result, the notification functions ignoring the agent capabilities, and the missing guard for an Edit that created or filled a file. - Build, lint and all 2362 tests pass.v0.85.2-preview.11 |
||
|
|
23b8ebf8c2 |
refactor: report tool calls on the experimental ACP v2 surface (#1231)
First part of step 4 of opt-in ACP v2 support (#1217, #1219, #1226, #1230): the v2 surface now sends the agent's tool calls. Nothing changes on ACP v1. ## Change `src/v2/tool-call.ts` maps the agent's v1 tool call reports to v2: - **One message.** v1 reports a new tool call with `tool_call` and changes it with `tool_call_update`. v2 has one `tool_call_update` that upserts by `toolCallId`, so both become that. - **`null` is left out.** Both versions leave an omitted field unchanged and replace `content` and `locations` as whole arrays. They differ on `null`: in v1 it leaves a field unchanged (v1's `name` docs say so explicitly), in v2 it clears it. The agent doesn't send `null` fields today; a unit test pins the rule. - **Content blocks pass through.** v2's content blocks are a superset of v1's. - **Diff and terminal content still throw**, until their own parts of step 4. ## Scenario suite Of the 30 scenarios that stopped at their first `tool_call`: | Status now | Scenarios | |---|---| | passes | 17: `read`, `grep-and-glob`, `bash-error`, `bash-background`, `skill`, `ask-user-question`, `memory-recall`, `permission-denied`, `tool-progress`, `task-output-and-stop`, four subagent scenarios, the three compaction scenarios | | stops at a permission request (step 5) | 9 | | stops at diff content (next part of step 4) | 4 | The 17 new golden files are in `src/tests/acp-scenarios/__snapshots__/v2/`. **One new check.** In v1, a `tool_call_update` for an id the client hasn't seen is an orphan, and clients drop it. In v2 the same message creates a tool call. A stray update would show up as a blank tool call, and the schema can't catch that. So every scenario recording, including those that stop, must give each tool call a title in its first report. It passes everywhere; making the first report lose its title fails it in 30 scenarios. ## Noted for later, not changed here - **`_meta` becomes stored state.** In v1, a report's `_meta` belongs to that report. In v2 it's stored on the tool call, and a concrete value replaces it. So a v2 client keeps only the last report's `_meta.claudeCode`: in `permission-denied`, the denial's `toolResponse` is replaced by the result's `{ toolName }`. No v2 client reads these keys yet. - **v2 has a `cancelled` tool status.** v1 reports an interrupted tool as `failed` with `_meta.claudeCode.nonExecutionKind`. Mapping it to `cancelled` means deciding where that fact lives. ## Verification Build, lint and all 2353 tests pass.v0.85.2-preview.10 |
||
|
|
2b7fab7d98 |
test: run the ACP scenarios on the experimental v2 surface (#1230)
Last part of step 3 of opt-in ACP v2 support (#1222, #1226): the scenario suite now also runs on the v2 surface. It's test-only. ## What `src/tests/acp-scenarios-v2.test.ts` runs all 40 scenarios of the ACP scenario suite the way `serveAcp` serves v2: through the protocol router, driven by the SDK's own v2 client. Per scenario: - `initialize`, then `session/new`, or `session/resume` with `replayFrom` for a transcript; - one `session/prompt` per turn, waiting for the turn's `idle`. The agent's JSON-RPC messages are recorded off the wire, so a message the client would reject is still recorded and checked. Three checks: 1. **Status table.** A scenario passes when every turn ends with a stop reason other than `_error`. Otherwise it records why it stopped: the rejected request's error, or the failed turn's notice. | Status today | Scenarios | |---|---| | passes | 7: `session-setup`, `text-and-thinking`, `goal`, `rate-limit-and-origin`, the three async-task scenarios | | `tool_call` not translated yet (step 4) | 30 | | `plan` not translated yet (step 5) | 2 | | replay not supported yet (step 4) | 1 | Each step that translates more updates must flip its entries to `passes`, so the table is that step's checklist. 2. **Schema.** Every recorded message, including those of scenarios that stop, validates against the SDK's draft v2 JSON schema. The definition is looked up from the method and side the schema itself names (`x-method`/`x-side`). One test checks that the validator rejects a chunk without `messageId`, so the check can't pass vacuously. 3. **Golden files** for the passing scenarios, in `src/tests/acp-scenarios/__snapshots__/v2/`. For example, `text-and-thinking.jsonl` shows the prompt lifecycle from #1226: `user_message`, `running`, the `session/prompt` answer, the chunks with their `messageId`, and `idle end_turn` with usage. `harness.ts` gains `runScenarioV2` next to `runScenario`, sharing the scripted Claude Code, the id normalization and the elicitation answer. `schema.ts` gains `validateV2Message` next to `validateRecorded`. ## Testing - Dropping `messageId` from the v2 chunk translation fails both the schema check and the golden files of the scenarios with chunks. - The v2 file is stable across repeated runs and takes about 1.3 s; the full suite stays at about 4.4 s. `npm run build`, `npm run check`, `npm run test:run` (2293 passed).v0.85.2-preview.9 |
||
|
|
d3e673b263 |
fix: end a held turn cancelled mid-followup at its interrupt's idle (#1229)
Found while reviewing #1226. It affects ACP v1 and v2. ## Problem A turn held open for its background subagents (`Turn.deferredSettle`, issues #864–#866) was settled `cancelled` inside `cancel()`, before the interrupt. The followup cycle can be live at that point, writing the subagents' summary or blocked on a permission request. The interrupt still flushes that cycle's output, so: - **v1:** the output arrives after `session/prompt` has answered `cancelled`, when many clients have stopped listening. - **v2 (#1226):** the output arrives after `idle cancelled`. The v2 draft says updates after a cancel MUST come before it. ## Change `cancel()` now settles inline only where the consumer can't: - **a settling turn,** blocked in its checkpoint preview, which can't process the interrupt's trailing idle (unchanged); - **a held turn whose session sits idle,** or has reported no state yet: the interrupt emits nothing, so no trailing idle comes (unchanged). **A held turn with a live followup** (`running` or `requires_action`) is now left to the consumer, like any live turn. It ends at the interrupt's trailing idle, or at the next prompt's echo if that idle lags, after the output the interrupt flushed. The force-cancel backstop is armed for it meanwhile. Two consequences, both handled: - **The trailing-idle debt:** the interrupted followup's result is autonomous, so it records its trailing idle as owed. When that idle ends the cancelled held turn, it now also pays the debt. Otherwise the debt would absorb a later issue-#825 signal. - **Usage:** a held turn can now be ended by the trailing idle, the echo hand-off, stream end or the backstop, not only by `cancel()`. All of them take the outcome from one rule, `cancelledOutcome`: a turn that already has its result keeps that result's usage and `_meta`, as the inline settle did (issue #844). Today the session's tally equals that recorded outcome anyway, since followup results aren't accumulated. Stream end already treats the stored one as authoritative. ## Testing - **New:** `src/tests/turn-events.test.ts` cancels a held turn mid-followup. The interrupt flushes more of the summary, then the followup's result and idle arrive. It asserts: - the flushed output comes before the turn ends `cancelled`; - a wedged next turn still fails at its un-owed idle (issue #825), so no trailer debt leaked. - **Updated:** the two existing tests that cancel a held turn in a live cycle (running, and blocked on a permission request) expected the prompt to resolve at `cancel()`. Their scripts send the interrupt's trailer only after the next prompt's echo, so the turn now ends at that echo hand-off. They keep their issue #825 assertion, and one now also checks the usage the turn keeps (issue #844). - **Unchanged:** the tests for an idle hold, which settles at `cancel()` without pre-counting a trailer, pass as before. - **Mutations:** settling a live held turn inline again, settling an idle one at an idle that never comes, or skipping the debt payment each fail at least one test. `npm run build`, `npm run check`, `npm run test:run` (2244 passed).v0.85.2-preview.8 |
||
|
|
d83f28a403 |
fix: drop queued prompts a cancel ends from Claude Code's queue (#1228)
Found while reviewing #1226: a prompt that `session/cancel` reports as cancelled still runs in Claude Code. It affects ACP v1 and v2. ## Problem `cancel()` settles every queued prompt as `cancelled`, but each was already pushed to Claude Code. Claude Code keeps queued messages through an interrupt and runs them next. The SDK documents the interrupt receipt's `still_queued` as "messages that WILL still run unless cancelled first". So: - Claude Code answers prompts the client was told were cancelled, and they stay in the conversation, and in the model's context, for later turns. - The orphan accounting skips their results, and a cancelled session drops consolidated `assistant` frames, but their streamed deltas still reach the client after the cancel. - In v2 (#1226), such a prompt is answered `-32800` ("not inserted"), then its output arrives anyway. ## Change `cancel()` now sends `cancel_async_message` for each queued message it abandons, before the interrupt. That covers queued prompts and pending steers. This is the pattern the SDK's own docs describe for a wrapper that wants per-uuid control, rather than `cancel_queued` on the interrupt, which would also sweep a prompt the client sends right after the cancel. - **Before the interrupt**, because once the interrupt stops the running turn, Claude Code starts the next queued message right away. - **Not awaited.** The SDK writes control requests to Claude Code in call order, so the drops still arrive before the interrupt, and a wedged CLI can't hold up the cancel, or the teardown that uses it. - **No new accounting.** A dropped message gets a `cancelled` lifecycle frame and is missing from the interrupt receipt's `still_queued`, and the orphan accounting already handles both. - **Residual window:** a message Claude Code had already dequeued, or one the SDK hadn't written yet, still runs as an orphan, exactly as before. The SDK implements the request as `Query.cancelAsyncMessage` (0.3.287) but doesn't type it, so it's looked up. A mocked query without it skips the drop. A CLI that doesn't know the request rejects it, which is logged. ## Testing A new test in `src/tests/turn-events.test.ts` scripts Claude Code's queue: a queued prompt runs after the interrupt unless it was dropped first, as the real CLI does. It asserts: - the drop goes out before the interrupt; - the queued turn ends `cancelled`; - the cancelled prompt's answer never reaches the client. Without the drop, the test fails with the leaked answer (`chunk Answer to a cancelled prompt` after `first ended cancelled`). Sending the drop after the interrupt also fails it. The rest of the suite passes unchanged. `npm run build`, `npm run check`, `npm run test:run` (2243 passed).v0.85.2-preview.7 |
||
|
|
ba5ed170ae |
refactor: report a turn inserted when Claude Code starts it (#1227)
Part of opt-in ACP v2 support. It fixes the first issue the review of #1226 found in the agent's turn reporting. It's independent of #1226 and changes nothing for ACP v1. ## Problem A turn reported `inserted` (#1222) at activation: the prompt's echo, or for an echo-less command, its result. The echo can come after the turn's first output; the agent already handles deltas that stream before it (see `injectSessionEchoAt` in the tests). With #1226's v2 mapping, that means a v2 client sees: - a turn's output before its `user_message` and before `running`; - `/compact`'s `session/prompt` answered only after the whole command. ## Change On CLIs that report command lifecycles (`msg_lifecycle_v1`), the `command_lifecycle` "started" frame now reports the turn inserted. **Why that frame is early enough.** I read the order in the CLI bundled with SDK 0.3.287 rather than in a live capture: - **A command that starts a fresh turn:** the engine pushes "started" into its lifecycle buffer at turn start (`C.push(W(w.uuid,"started"))`, before `[engine] turn N start`). The turn's message loop then flushes that buffer before it yields anything the turn produced (`if(C.length>0)yield*te()`), including its echo and first `stream_event`. - **A prompt folded into a running turn:** its `queued_command` attachment (the echo) is yielded before "started", so activation at the echo already comes first and this frame changes nothing. **One guard.** While another turn is still active, insertion still waits for the echo. That covers a turn held for its background work, a steered turn, and a cancelled turn awaiting its trailing idle. The echo's hand-off ends that turn first, so a turn is never reported inserted before the previous one ends. **Older CLIs** without lifecycle frames keep insertion at activation. Only the time `inserted` is reported changes. Activation, output attribution and settlement don't, and ACP v1 ignores `inserted`. ## Testing `src/tests/turn-events.test.ts`: - a fresh turn whose answer streams between "started" and its echo is reported inserted before that output; - a prompt that starts while the turn before it is held is reported inserted only after that turn ended. Removing the new insertion point fails the first test, and removing the guard fails the second. The rest of the suite passes unchanged, including the recorded v1 scenario traffic. `npm run build`, `npm run check`, `npm run test:run` (2231 passed). |
||
|
|
d24272c6b0 |
refactor: serve ACP v2 prompts through the turn events (#1226)
Second part of step 3 of opt-in ACP v2 support (#1217, #1219, #1222). The v2 surface now serves `session/prompt` and `session/cancel`. It still doesn't advertise `capabilities.session`; that waits for tool calls and replay (step 4). ## What `src/v2/prompt.ts` serves a v2 prompt through the agent's turn events (#1222), instead of translating it into a v1 prompt: | Turn event | v2 | |---|---| | `inserted(messageId)` | `user_message` (the prompt as received, under the uuid Claude Code echoed), `state_update: running`, then the answer `{ messageId }` | | `awaitingUser()` / `resumed()` | `requires_action` / `running` | | `ended(outcome)` | `idle` with the stop reason, `usage`, and `_meta` | | `ended` before insertion | `-32800`: a queued prompt that a cancel ended | | `failed` before insertion | the error as the JSON-RPC answer | | `failed` after insertion | an error `notice` that shows it, then `idle` with the custom stop reason `_error` and v1's JSON-RPC error in `_meta.claudeCode.error` | The last row is a gap in the draft: the idle `state_update` has no error, its stop reasons are all non-error ones, and a notice "must not carry … fatal-error reporting". So the failure travels on `idle` as a `_`-prefixed custom stop reason, which the draft allows, with the error in `_meta`; `auth_required`'s code survives there. The notice shows the agent's own failure title ("API Error: 529 Overloaded"); `TurnEvents.failed` gains an optional `title` for it. That's going back to the v2 draft as feedback. - `session/cancel` maps to the v1 cancel. - **Ordering:** the turn's state updates and the agent's own updates go through one `ctx.notify` path in call order, so `idle` comes after the turn's last output. The tests check the order. - **Message chunks** (`agent_message_chunk`, `agent_thought_chunk`, `user_message_chunk`) are translated with the `messageId` v2 requires, and fail loudly without one. - **Prompt content:** blocks that v1 can't express (a custom or future type) are rejected with `-32602`. Audience roles that v1 doesn't know are dropped from annotations, which the agent doesn't read. - Tool call and plan updates still fail loudly until their step. ## The one change on v1 The agent built three message chunks without a `messageId`: local command output, a result forwarded as the answer (a local-only command, or the issue #453 fallback), and a refusal explanation. Each now carries the uuid of the SDK message it came from, the id `messageIdForGrouping` already uses for non-assistant messages. It's an additive field on v1. None of the recorded v1 scenario baselines contain these chunks, so they don't change. ## Testing `src/tests/acp-v2.test.ts` drives the router with the SDK's v2 client, which validates every message against the v2 schema. Its mocked Claude Code now answers prompts: each turn echoes its prompt, then runs a script. New tests: - a prompt is answered with the echoed uuid while its turn still runs, then `agent_message_chunk` and `idle end_turn` with usage; - a queued prompt stays unanswered until the turn before it ends, then is answered with its own echoed uuid; - an MCP elicitation during the turn gives `requires_action`, then `running`; - `session/cancel` follows a real interrupt: output Claude Code sends after it arrives before `idle cancelled`, and a queued prompt is answered `-32800`; - a turn whose result is an API error gives an error `notice` with the agent's title, then `idle _error` with the error code; a missing login keeps `auth_required`'s code; - a queued prompt whose stream ends before insertion is answered with the error; - a refusal explanation carries the refusing result's uuid; - local command output and a forwarded result each arrive with their SDK message's uuid as `messageId`; - a prompt with a `_acme/widget` block is rejected and never reaches Claude Code; - a chunk without a `messageId` fails translation, and audience roles v1 doesn't know are dropped. Mutation check: answering at the end or at queueing instead of at insertion, dropping `user_message`, answering a cancelled queued prompt, dropping the failure notice or its title, ending a failure with `end_turn` or without the error, dropping `requires_action`, removing any of the three new `messageId`s, accepting any content block, or swallowing a failure before insertion each fails at least one test. `npm run build`, `npm run check`, `npm run test:run` (2240 passed). ## Not in this PR A review of this PR found these in the agent's turn reporting and cancellation, outside the mapping. They're tracked for their own PRs before the v2 surface is advertised; the first three also affect v1: - insertion is reported at the echo, which can lag the turn's first output; - a cancelled queued prompt's streamed output still reaches the client; - cancelling a held turn settles it before the interrupt's remaining output; - `session/close` answers before the active turn's `idle cancelled`; - whether a held turn should delay `idle` in v2 at all.v0.85.2-preview.6 |
||
|
|
190a00f68f |
feat: answer /mcp in chat with the MCP server status and a reconnect (#1218)
## Summary In an ACP client, `/mcp` showed the CLI text "… Use `/mcp` in the terminal for details." The user of an ACP client has no terminal. In SDK mode, Claude Code also refuses `/mcp reconnect|enable|disable` with "Reconnect, enable, and disable aren't available in this session." and changes nothing. - `/mcp` and `/mcp reconnect|enable|disable [<server>|all]` still run as a normal Claude Code turn. The adapter replaces the text of the local command, through the same hook that `/usage` uses. Prompt order, cancel, and the provider update stay with the turn. - For an action, the adapter runs it in that turn through the SDK control API (`reconnectMcpServer`, `toggleMcpServer`). It shows one result line for each server, then the status list. - The status list comes from `query.mcpServerStatus()`. It has a summary line and groups by status: failed, needs authentication, connecting, connected, and disabled on one line. A failed server shows a short cleaned error. A list is used and not a table, because a table breaks words in narrow columns. - Useful CLI sentences stay, for example "There's no MCP server named …". Sentences that point to the terminal are removed. - After an explicit reconnect, a server from the ACP session that needs authentication starts the existing MCP OAuth flow, when the client supports URL elicitation. - `/mcp` is advertised with the input hint `[reconnect|enable|disable [<server>|all]]` and is hidden on history replay. ## Other commits in this branch This branch also carries commits of other work: skill command metadata for AIR, a faster `session/load` replay, and the resumed permission mode. They can move to separate PRs if preferred. ## Tests - `src/tests/mcp-command.test.ts` (61 tests). The fake CLI answers with the real SDK-mode texts and message shapes (synthetic assistant + result) and does not change any status. - A live run through the adapter with a broken and a working stdio server: `/mcp`, `/mcp disable good`, `/mcp enable good`, `/mcp reconnect broken`, `/mcp reconnect nosuch`. - `compare.ts` allows one new difference against the origin/main baseline: the `mcp` command in `available_commands_update`. The baseline files are unchanged. - `npm run check` and `npm run test:run` pass.v0.85.2-preview.5 |
||
|
|
ddd5f54187 |
feat(deps): Bump @anthropic-ai/claude-agent-sdk to 0.3.287 (#1224)
## Summary
- Bumps `@anthropic-ai/claude-agent-sdk` to 0.3.287 (Claude Code
2.1.287). No compile or test fixes were needed.
## Review notes for 0.3.287
- **WebFetch/WebSearch keep running when a steer arrives: not active for
the adapter.** In 2.1.287, a human-origin `priority: 'now'` steer no
longer cancels WebFetch or WebSearch. The call keeps running, and its
tool_result is a "[Still running…]" notice for the model with
`tool_use_result: {detachedToolCall: true}`. The steer gets its own
result, and the CLI holds back idle. When the fetch finishes, Claude
starts a follow-up turn on its own. That turn has no user message and
its result carries no uuid, and idle only arrives after it. I checked
this against the live CLI. However, the CLI only does this when no
PostToolUse, PostToolUseFailure or PostToolBatch hook applies to the
tool. The adapter's PostToolUse hook has no matcher, so it applies to
every tool, and through the adapter the steer still cancels the fetch,
as before. I also checked that end to end. Turning the new behavior on
needs three changes: limit the hook to
Write/Edit/NotebookEdit/EnterPlanMode, render the card while the call
keeps running, and close the card at the follow-up turn. That is left
for a follow-up PR.
- **No changes needed:**
- the `includePartialMessages` late or missing `message_stop` fix (the
adapter's `message_stop` handling can safely run more than once)
- the `commands_changed`-before-`init` fix
- the clearer revoked-OAuth error text (it still arrives as an
`authentication_failed` error)
- MCP `structuredContentOmitted` (the adapter doesn't read
`structuredContent`)
- the `alwaysLoad: false` semantics (unused)
- the remote-only `first_text_post_*` result fields
- `get_task_output` (in the types, but `Query` has no method for it)
- the fix for a model fallback repeating on every later message
## Test plan
- [x] `npm run build`
- [x] `npm run test:run` (2142 passed)
- [x] `npm run check`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
v0.85.2-preview.4
|
||
|
|
3d4017b15a |
test: keep the suite off the real Claude CLI (#1223)
Fixes an intermittent failure of "replays a subagent from its own transcript file", and with it a broader leak: the unit suite was running the real Claude CLI hundreds of times per run. ## Cause The agent runs `claude auth status --json` in the background, without awaiting it, on every `initialize` and prompt. The scenario suite stubs it (`CLAUDE_CODE_EXECUTABLE=/usr/bin/false`), but other files didn't. Logging the probe's executable over one `vitest run` showed about 670 runs of the real CLI: | File | Real CLI runs | |---|---| | `acp-agent.test.ts` | 375 | | `create-session-options.test.ts` | 14 | | `incomplete-tools.test.ts` | 12 | | `session-titles.test.ts` | 8 | | `file-change-audit-integration.test.ts` | 5 | Each run uses the machine's own Claude config, unless a test points `CLAUDE_CONFIG_DIR` elsewhere. That flaky test points `CLAUDE_CONFIG_DIR` at a temp dir. The CLI it started then wrote `.claude.json`, `.claude.json.lock` and `backups/` into that dir about a second later, after the test body had finished. Under full-suite load, that overlapped the `rm` in the test's `finally`, which failed with `ENOTEMPTY`. Its assertions always passed. ## Change `vitest.config.ts` sets `CLAUDE_CODE_EXECUTABLE=/usr/bin/false` in `test.env`: - It's the override `claudeCliPath()` already honors, and the value the scenario suite already uses, so production code doesn't change. - `test.env` is the base environment, so it survives the `vi.unstubAllEnvs()` that three files call. - Runs that exercise the real CLI keep it: with `RUN_INTEGRATION_TESTS` (`npm run test:integration`) or `RUN_LIVE_CAPABILITY_AUDIT=true`, the variable is left unset. Tests that mock `execFile` or set their own executable are unaffected. ## Evidence - After the change, the same logging shows 0 runs of the real CLI: 688 probes run `/usr/bin/false`, and 108 hit files that already mock `execFile`. - The flaky test's temp dir now holds only the test's own `projects/` after the test, with no `.claude.json`. - Before the change, it failed in about 2 of 6 full runs on my machine. After, 8 of 8 full runs passed. - A full run takes about 4.8 s instead of 8.4 s. - The variable is `/usr/bin/false` in a default run and unset with either opt-in flag. - `npm run build`, `npm run check`, `npm run test:run`.v0.85.2-preview.3 |
||
|
|
6b69852cfc |
refactor: report each prompt turn through internal turn events (#1222)
First part of step 3 of opt-in ACP v2 support (step 1: #1217, step 2: #1219). It's independent of #1219. It changes no ACP v1 behavior, apart from one failure-only case noted below. ## Why ACP v2 changes the prompt lifecycle. `session/prompt` is answered when the prompt is *inserted* into the conversation, and progress and the stop reason arrive as `state_update`s. In v1, the response comes at the end of the turn. The agent only exposed the v1 shape: `prompt()` returns at the end of the turn. So v2 can't be mapped from the outside. Following the discussion on #1219, the adapter gets its own view of a turn, and each ACP version maps to it, instead of v2 being translated into v1. ## What `src/turn-events.ts` defines the adapter's own turn lifecycle: - `inserted(messageId)`: Claude Code took the prompt in as this user message. At most once, and never after the turn ends. - `awaitingUser()` / `resumed()`: a permission request or question is open in the session / none is open any more. - `ended(outcome)`: with a `TurnOutcome` (`stopReason`, `usage`, `_meta`). - `failed(error)`. The agent calls these synchronously while it updates its own state, so handlers must not throw or call back into the agent. In the agent: - `startTurn(params, events)` starts a turn. It resolves once the prompt is queued for Claude Code, and rejects only if the prompt is refused before that. Events can arrive before it resolves. `prompt()` is now the v1 view of it: it awaits the turn's outcome. - **inserted** is reported in `activateTurn`, the single place a queued turn becomes active. That's when Claude Code takes the prompt in: its echo, or for an echo-less command, its result. Two guards: - a clear-context restart re-activates the same turn in the fresh session, so it's reported once; - a turn that a cancel settled during the previous turn's hand-off isn't reported at all. - **awaiting user / resumed** follow one session-level rule: the active turn awaits the user exactly while any request in the session is open, rechecked when a turn activates. A request carries no prompt id, and Claude Code can ask for a queued prompt before the consumer reaches its echo. So tying a request to the turn that was active when it opened would misattribute it. - **ended / failed** come from the existing settle points, each guarded by `settled`, so a turn reports exactly one of them. - `TurnOutcome` and `StopReason` are the adapter's own types. A turn's outcome is field for field a v1 prompt response, so `PromptResponse` now only appears at the v1 boundary. **The one v1 difference:** a prompt that sets a goal publishes it to AIR clients after the prompt is queued. If that notification fails, `startTurn` now logs it rather than rejecting, because the turn is already running. For v1 this only matters on a dead connection: `prompt()` waits for the outcome instead of rejecting with the write error. ## Notes for the v2 step - A prompt sent while another turn is running is inserted only when Claude Code takes it in, after the running turn. The open question about queued prompts is going back to the v2 draft. - An echo-less command (such as `/compact`) is inserted at its result, after its output. On `msg_lifecycle_v1` CLIs, the `command_lifecycle` "started" frame may be an earlier insertion point. - A queued turn that a cancel ends before insertion may still be run by Claude Code (the orphan accounting). - Any open request counts towards `requires_action`, a background task's included; `agentID` could refine that in the permission step. ## Testing `src/tests/turn-events.test.ts` covers: - a normal turn: inserted under the uuid of the message Claude Code received, then ended; - a permission request: awaiting user, then resumed; - a request open from before the turn: the turn awaits the user from insertion, through its own answered request, until the earlier one is answered too; - a cancel during the hand-off from the previous turn: the queued turn ends as cancelled and is never inserted; - cancelling while a second prompt is queued: the active turn is inserted, then ended as cancelled, and the queued turn ends as cancelled without being inserted; - the stream ending with a turn queued: it fails without being inserted; - a prompt for an unknown session: `startTurn` rejects and reports nothing. The existing clear-context restart test also checks that the restarted turn is inserted once. Each guard is checked by mutation: removing the `insertedReported` guard, the `settled` guard, the activation recheck, or the change-only dedupe; tying `resumed` to the request's own turn; or reporting insertion at enqueue each fails at least one test. The existing suite passes unchanged, including the 474 scenario tests that compare outbound v1 traffic with recorded baselines. `npm run build`, `npm run check`, `npm run test:run`.v0.85.2-preview.2 |
||
|
|
c60fa48fd8 |
refactor: serve the ACP v2 session lifecycle without prompts (#1219)
Step 2 of opt-in ACP v2 support (step 1: #1217). Behind `CLAUDE_AGENT_ACP_EXPERIMENTAL_V2=1`, the v2 surface now serves `session/new`, `session/list`, `session/resume` (without replay), `session/close`, `session/delete`, and `session/set_config_option`. ACP v1 is unchanged. `capabilities.session` is still not advertised. Under v2, advertising it commits the agent to the whole baseline, including `session/prompt` and `replayFrom: start` replay, and those come in later steps. Until then these methods are served but not advertised. ## Translation As in step 1, each v2 request becomes the v1 request that `ClaudeAcpAgent` already serves, and each v1 response or update becomes its v2 form (`src/v2/session.ts`, `src/v2/session-update.ts`): - **MCP servers:** v2 requires a `type` on every transport, but the agent recognizes a v1 stdio server by its *missing* `type`. A v2 `type: "stdio"` config passed through as is would be silently skipped, so the tag is dropped. The lists v1 requires (`args`, `env`, `headers`) are defaulted. A transport v1 can't express is rejected with invalid params instead of being left out of the session. - **Config options:** `id` → `configId`, a select group's `group` → `groupId`. A select value in `session/set_config_option` drops its v2 `type: "id"`, and value types v1 can't express are rejected. - **Modes:** responses drop `modes`, and `current_mode_update` is dropped. The agent always lists the mode as the `mode` config option, and every mode change also reaches the client as a `config_option_update` or in the `set_config_option` response. - **Commands:** `available_commands_update` inputs gain `type: "text"`. - **Notices, session info, usage:** passed through unchanged. v2 needs no capability for notices, so a v2 client is reported to the agent as taking them, and advisories arrive as `notice` instead of transcript messages. - **Elicitation:** the same in v1 and v2, so it's forwarded. MCP OAuth at `session/new` works. - Update kinds that aren't translated yet (messages, tool calls, plans, extensions) throw, so a gap shows up as an error, not a missing update. - `replayFrom` is rejected until replayed history can be translated. Also bumps `@agentclientprotocol/sdk`: - to 1.6.1, whose router types `closed` as always present, so `serveAcp` drops its `closed!`; - to 1.7.0, which type-checks the v2 extensible-union values the adapter builds, at any depth ([typescript-sdk#268](https://github.com/agentclientprotocol/typescript-sdk/pull/268)). The v2 translations compile unchanged. The exception is `createElicitation`, which relays the v1 request: v1 types a property schema's tag as any string, and an MCP server's schema passes through unchecked, as on v1. It gets a cast with a comment saying so. ## Testing - `src/tests/acp-v2.test.ts` drives the router with the SDK's v2 client app, which validates every response, session update, and elicitation against the v2 schema. New cases cover session creation with v2 MCP servers, config options, and commands; rejected MCP transports; list/close/resume/delete and rejected `replayFrom`; setting the mode with no `current_mode_update` reaching the client; notice support; MCP OAuth through URL elicitation; and unit tests for grouped options, rejected value types, and untranslated updates. Breaking the stdio or command-input translation makes the tests fail. - Ran the built binary over stdio against a real Claude Code session: v2 `initialize`, `session/new`, `list`, setting the mode, and `close` all passed the v2 client's validation, and the 52 real commands arrived with `type: "text"` inputs. `session/new` took ~0.7 s, the same as over v1. - `npm run build`, `npm run check`, `npm run test:run`.v0.85.2-preview.1 |
||
|
|
686c0c99b3 |
chore(main): release 0.85.1 (#1213)
🤖 I have created a release *beep* *boop* --- ## [0.85.1](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.85.0...v0.85.1) (2026-10-01) ### Bug Fixes * answer session/close without waiting for the interrupt reply ([#1216](https://github.com/agentclientprotocol/claude-agent-acp/issues/1216)) ([v0.85.1 |
||
|
|
8e893f51ea |
refactor: add an opt-in experimental ACP v2 handshake (#1217)
First step toward optional support for the [draft ACP v2 protocol](https://agentclientprotocol.com/protocol/v2/draft/overview). ACP v1 stays the default and is unchanged. ## What it does Set `CLAUDE_AGENT_ACP_EXPERIMENTAL_V2=1` and the adapter routes each connection by the protocol version the client requests in `initialize` (via the SDK's `AgentProtocolRouter`): - A client that requests version 2 or later gets the draft ACP v2 `initialize`, `auth/login`, and `auth/logout`, plus the `_auth/status_update` push. - Every other client gets ACP v1, served by the same handlers as today. Without the variable, the adapter never builds the router and serves ACP v1 exactly as before. The v2 response does not advertise `capabilities.session` yet. A v2 agent that advertises it must serve the whole session baseline (`session/new`, `list`, `resume`, `close`, `prompt`, `cancel`, `update`), which follow-up PRs add. So for now a v2 client can initialize and authenticate, but cannot open a session. ## How `ClaudeAcpAgent` keeps speaking ACP v1 types. The v2 surface in `src/v2/` is a thin translation layer around it, so v1 and v2 share one implementation: - `v2AgentApp` registers the v2 methods and translates each request to the v1 request the agent already serves, and each response back. For `initialize` that means `info`/`capabilities` instead of `agentInfo`/`agentCapabilities`, and auth methods with `methodId` and a required `type`, with terminal `env` as `{ name, value }` pairs. A v2 client gets no client `fs` or terminals, and boolean config options are baseline. - `V2ClientConnection` is the `AcpClient` of a v2 connection. It sends the agent's v1-shaped messages in their v2 form. Only connection-level messages can reach it so far; session-scoped methods reject until v2 sessions land. - `v1AgentApp` is the existing v1 handler registration, pulled out of `runAcp` so the router can reuse it. Each surface now creates the agent of its connection in `onConnect`, because under the router the connection opens only after `initialize` has been read. `runAcp` (a public export) behaves as before. Also bumps `@agentclientprotocol/sdk` to 1.6.0 (schema v2.0.0-alpha.7). The bump changes nothing for v1: the full suite passes unchanged on it. ## Testing - `src/tests/acp-v2.test.ts` drives the real router over in-memory streams with the SDK's own v1 and v2 client apps (the v2 client validates every response against the v2 schema). It covers the v1 handshake through the router, the v2 handshake, method-not-found for v2 session methods, the `_auth/status_update` push, and gateway `auth/login` followed by `auth/logout`. - Ran the built binary over stdio: v2 client with the flag, v1 client with the flag, v2 client without the flag (gets the unchanged v1 response), and stdin EOF before `initialize` (exits 0). - `npm run build`, `npm run check`, `npm run test:run`.v0.85.1-preview.4 |
||
|
|
0c862e2949 |
fix: answer session/close without waiting for the interrupt reply (#1216)
## Summary - `teardownSession` sends the interrupt but does not await its reply - `query.close()` follows at once and ends the CLI process - `session/cancel` still awaits the interrupt reply On a fresh session, the background `getContextUsage` request from `refreshContextWindowInBackground` is still in flight. The SDK serializes control requests, so the CLI answered the interrupt only after it. With a real CLI, `session/close` took 2.5 s in an empty directory and 3.2 s in a large repository. ACP clients with a 3 s close timeout timed out. With this change, close answers in about 2 ms, and the CLI process exits. ## Tests - `npm run test:run -- src/tests/create-session-options.test.ts` - `npx tsc --noEmit` - `npm run check`v0.85.1-preview.3 |
||
|
|
4ffa625977 |
fix: restore background task stops on replay instead of showing them as prompts (#1205)
When a background task stops, the CLI persists a `<task-notification>` user record. Live, the adapter hides it and handles the SDK task lifecycle frame; replay previously showed the raw XML as a user prompt. Replay now hides notification records and removes embedded notification XML while preserving adjacent user text and delivered prompts with an origin `subkind`. It restores known non-agent tasks through `AsyncTaskRuntime`, including `killed` → `stopped` and duplicate terminal-event suppression. Agent notifications override the launch result's terminal state. Unknown tasks remain hidden without creating a task card. Task launches are recognized in `tool_use`, `server_tool_use`, and `mcp_tool_use` blocks. Async task updates use the session that owns the launching tool call, even when another session receives the notification. History has no `task_started` frame, so tasks without a persisted stop notification cannot be reconstructed. The large Write patch test now uses a fixed clock to check patch correctness independently of CI speed; a separate advancing-clock test verifies the existing timeout fallback. The production 100 ms budget remains unchanged. Validation on current main (SDK 0.3.286): - `npm run build`: passed. - `npm run check`: passed. - `env -u ANTHROPIC_BASE_URL npm run test:run`: 2119 passed, 29 skipped. The local gateway environment otherwise changes the expected default URL in four provider tests. - Regression coverage includes origin/text notifications, adjacent user text, unknown and repeated tasks, killed tasks, background Agents, delivered subkind prompts, alternate tool block types, task session ownership, and patch timeout fallback.v0.85.1-preview.2 |
||
|
|
7f8d5471e2 |
fix: fail turns with unfinished tools (#1212)
Fixing silent agent turn completion in case of tools failurev0.85.1-preview.1 |
||
|
|
c84845272f |
chore(main): release 0.85.0 (#1209)
🤖 I have created a release *beep* *boop* --- ## [0.85.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.84.0...v0.85.0) (2026-10-01) ### Features * **deps:** Bump @anthropic-ai/claude-agent-sdk to 0.3.286 ([#1211](https://github.com/agentclientprotocol/claude-agent-acp/issues/1211)) ([v0.85.0 |
||
|
|
af0b43de94 |
fix: send the legacy custom answer key to AIR (#1210)
Released AIR versions, such as 262.air/release-09, read the custom answer companion only from the root key _meta._askUserQuestionCustomAnswer. The adapter sent only _meta.jetbrains.air.customAnswer, so these versions showed the companion as a separate question. For an AIR client the companion now carries both keys with the same value. Other clients get no key, as before. Remove the root key when the ACP registry no longer serves AIR versions that read only that key. IJAI-2892v0.84.1-preview.3 |