mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-11-11 20:01:11 +00:00
c331e178a5df47448be1a3ed2f71270c3aad2433
656 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c331e178a5 |
fix: explain why Claude Code turns Fast mode off (#1602)
When the user turns Fast mode on, Claude Code can report
`fast_mode_state: "off"` on the next turn's `system`/init. The adapter
mirrored that into the option but explained it only for reasons listed
in `FAST_MODE_UNAVAILABLE_EXPLANATIONS`. `preference`, `unknown` and any
newer code were dropped, so the toggle snapped off with no reason. This
happens on every turn behind an API gateway that authenticates with
`ANTHROPIC_AUTH_TOKEN` only: Claude Code treats Fast mode as disabled by
the organization (`preference`) without asking the server.
Changes:
- `preference` ("not enabled for your organization or API gateway") and
`unknown` ("currently unavailable") are explained. A reason code the SDK
adds later gets a generic `not available (<code>)`.
- Only `sdk_opt_in_required` (every SDK session starts there; the toggle
is the opt-in) and `pending` (eligibility still resolving) stay
unexplained.
- The notice rule is unchanged: Fast mode was on, then off with an
explained reason. Off without a reason stays quiet, because a `result`
can carry the state from the start of the turn.
- One shared function in `session-fast-mode.ts` serves v1 and v2. In
`src/v1/acp-agent.ts` only comments change.
Limitation: the adapter still offers the toggle up front. Until a
session opts in, Claude Code reports `sdk_opt_in_required` and hides the
account or organization blocker, so the adapter can't tell in advance
that Fast mode will be refused.
Tests: unit tests for each reason, plus v1 and v2 flow tests (set `fast`
on, prompt, init reports off/`preference`). The flow tests fail without
the fix.
|
||
|
|
830f05a0ac |
refactor: move the v1 session fingerprint out of acp-agent.ts (#1601)
Moves `computeSessionFingerprint` and its helpers (`normalizeSkills`, `OPTION_REBUILDS_SESSION`, `canonicalJson`) verbatim from `src/v1/acp-agent.ts` into `src/v1/session-fingerprint.ts`. No behaviour change. - Only `computeSessionFingerprint` is exported, as before; the helpers stay private to the new module. - `session-fingerprint.ts` imports nothing from `acp-agent.ts`. The test that used `computeSessionFingerprint` now imports it from the new module. - Emitted JS differs from `main` only in `dist/v1/acp-agent.js` (one import, the moved code removed) plus the new module. Review with `git diff --color-moved` to see the move. |
||
|
|
4df69996ca |
fix: report a mid-turn API error notice where the response broke off (#1599)
## Problem When a response stream drops after some blocks arrived, Claude Code keeps the partial response and writes its own message into the conversation, e.g. "API Error: Connection lost mid-response. The response above may be incomplete." If the partial response contained tool calls, Claude Code runs them and the turn goes on. Since #1582 the v2 agent sends this text as a `notice`, but only at the turn's result. In a turn that goes on, the notice arrived after all the later work, so "the response above" pointed at the wrong output. ## Change The v2 agent sends the pending notice as soon as the main conversation goes on past the failed request: at the next tool result, stream event or assistant message. A turn that ends at the failed request behaves as before: the idle's `error` carries it when the turn fails, and a notice goes out at the turn's end otherwise. ## Not changed - v1 keeps its current behaviour, as in #1582. - `session/load` replay still shows these messages as text. ## Testing - New v2 case: a kept tool call, the API error message, the tool result, then more output. The notice comes before the later text. It fails without the change. - The test double follows the message shape and order read from the bundled CLI's code. Not run against a real dropped connection. - `npm run check`, `npm run build` and `npm run test:run` pass. |
||
|
|
ecd066c870 |
fix: say why the login failed in the v2 authRequired error (#1598)
## Problem When an API request fails authentication, the v2 agent hides the message Claude Code writes for it (#1582) and fails the turn with a bare `Authentication required`. The reason is lost, for example: > Failed to authenticate: this login reached its time limit and cannot be renewed. Run `claude auth login` to sign in again, or use `claude setup-token` and set CLAUDE_CODE_OAUTH_TOKEN for unattended jobs A client can then only say that the agent is signed out, not why. ## Change The turn's `authRequired` error now carries the result's error text in its message: `Authentication required: <text>`. The code and `data` are unchanged, so a client that starts its own sign-in from the code behaves as before. Text that tells the user to run `/login` is still left out, as that instruction is for Claude Code's terminal (#863). Only the v2 agent changes. ## Testing `npm run check`, `npm run build` and `npm run test:run` pass. The v2 tests now pin both messages: the plain one for the `/login` text, and the one with the reason for an authentication failure reported on the assistant message. Not checked against a real expired login. |
||
|
|
a22a797db2 |
refactor: move the v1 steering and async task stop requests out of acp-agent.ts (#1581)
Moves the `_session/steering` and `_session/async_task/stop` request definitions verbatim from `src/v1/acp-agent.ts`, like `goal-extension.ts` does for goals. No behaviour change. - `src/v1/steering.ts`: `STEER_METHOD`, the steer priorities, `SteerRequest`, `SteerResponse`, `parseSteerRequest`. - `src/v1/async-task-stop.ts`: `ASYNC_TASK_STOP_METHOD`, its request and response types, `parseAsyncTaskStopRequest`. - Neither module imports `acp-agent.ts`. The one test that imported `SteerRequest` from `acp-agent.js` now imports it from `steering.js`. - Emitted JS differs from `main` only in `dist/v1/acp-agent.js` (imports and the moved code) plus the two new modules. Review with `git diff --color-moved` to see the move. |
||
|
|
4f690e129c |
feat: say the cause of an api retry in the AIR warning details (#1594)
## Summary The AIR warning for an SDK `api_retry` message had only a title, such as `Retrying Claude, attempt 2 of 10.`. The user did not see why Claude retries or how long the next attempt waits. The warning now carries `details` with the cause and the wait. ## Changes - `apiRetryWarning(message)` in `src/session-failure-extension.ts` builds the title and the details. The title text does not change. The `api_retry` case of `src/v1/acp-agent.ts` only calls it. - The cause: - `HTTP 529 (overloaded).` when the CLI sends a status; - `Connection error: no HTTP response.` for a null status with the `unknown` kind; - `No HTTP status (overloaded).` for a null status with a kind. The CLI sends `overloaded` with no status for an SSE `overloaded_error` event after HTTP 200, so "no HTTP response" would be wrong there. - The wait: `Next attempt now.` for 0 ms (a `no_response` retry), `Next attempt in 8s.`, and whole minutes from 120 s (a background rate-limit wait), e.g. `Next attempt in 30m.`. - The `no_response` wait stays in the title only. - The kind label map has the type `Record<Exclude<SDKAssistantMessageError, "unknown">, string>`, so a new SDK kind fails typecheck. A missing `error` reads as `unknown`. - The 401 skip, the actions and the terminal failures do not change. The subagent retry path sends a `tool_progress` update and no session failure, so it does not change. v2 has no `api_retry` session failure. ## Tests - `src/tests/session-failure-extension.test.ts` (new): 0 ms, minutes, the 119 s / 2 m boundary, a null status with `cloud_credential_error`, and a missing `error`. - `src/tests/v1/acp-agent.test.ts`: the warning details on the wire, revision 2 replaces the details, and the terminal revision has none. The test doubles use status and kind pairs and delays that the bundled CLI really sends. - `npm run build`, `npm run check`: clean. - `npx vitest run src/tests/session-failure-extension.test.ts src/tests/v1/acp-agent.test.ts`: 766 passed, 9 skipped. - `npm run test:run`: 17 tests in `create-session-options.test.ts`, `providers.test.ts` and `acp-v2.test.ts` fail on `main` without this change too. They read the model list of the local environment. |
||
|
|
b1763c750a |
feat: serve session/options and initial config options (#1592)
Implements the unstable [Session options RFD](https://agentclientprotocol.com/rfds/session-options) on ACP v1 and v2. Both cores advertise the `options` session capability. ## `session/options` Answers with what a session would start with: modes and config options on v1, config options and commands on v2. - A session that the connection holds answers with what it has now. - Otherwise a query starts as the session's would, without MCP servers, and closes once its options are read. With `sessionId` it resumes that session, and an unknown one fails with `-32002`. - It sends no update and registers no session. Claude Code only reports its models, modes and agents from a running process, so there is no way to answer without starting one. The query runs with `persistSession: false`, so it stores no session and does not write to a resumed transcript. Two consequences: each call costs a CLI start, and the user's `SessionStart` hooks run in it. The v2 commands lack MCP prompts, which the RFD allows. On v1 this is an `optionsOnly` exit in `createSession`, after the config options are built and before the session is registered. On v2, `launchSession` is split into `openQuery` (start the query, build the config) and the part that makes the session active, and `session/options` uses only the first. ## Initial `configOptions` `session/new`, `session/load` (v1) and `session/resume` take `configOptions`. They are set in list order through the same code as `session/set_config_option`, so a model entry decides which effort levels a later entry can pick. - Two entries with the same `configId` fail with `-32602` before a query starts. So does a value type that v2 does not take. - An entry with an unknown `configId`, a value of another type, or a value the option does not offer is skipped. An entry that Claude Code refuses is skipped and logged. - No update goes out for the entries. The response carries the modes and options, and the warning of an Auto mode fallback waits for the next prompt, as it does at a session's start. `session/fork` does not take them yet: the schema has no field for it. agentclientprotocol/agent-client-protocol#2794 adds one. ## Testing New tests in `src/tests/v1/create-session-options.test.ts` and `src/tests/v2/v2.test.ts` cover both versions. The scenario golden files change only by the new capability in `initialize`. Not verified against a real Claude Code yet: `persistSession: false` together with `resume`. The tests run against the mocked SDK. |
||
|
|
f6d6429187 |
feat: report the auth detail as the account's description (#1591)
Stacked on the SDK 2.6.0 update (`acp-sdk-2.6.0`). Merge that first. `auth/status` and `auth/status_update` carried the detail of an account, such as the key source or the gateway host, in parentheses in its label: `Anthropic API key (ANTHROPIC_API_KEY)`. SDK 2.6.0 adds `AuthAccount.description` for it, so the account is now `label: "Anthropic API key"` with `description: "ANTHROPIC_API_KEY"`. `_meta.claudeCode.detail` still carries the detail for clients that read it there. A client that shows only `label` no longer shows the detail. The mapping is shared, so v1 and v2 both change. `docs/air-extensions.md` is updated. |
||
|
|
9fca22a03b |
fix: update to @agentclientprotocol/sdk 2.6.0 (#1590)
Updates `@agentclientprotocol/sdk` from 2.5.0 to 2.6.0 (schema v1.31.0 and v2.0.0-alpha.15). The new schema adds `configOptions` to session setup requests. The v2 core converts a setup request to its v1 form, and the v2 entries are not assignable to the v1 type, so `v1NewSessionRequest` leaves them out. Nothing reads them yet: a Client only sends them to an Agent that advertises `session.options`. #1592 serves them. No behavior change. `npm run check`, `npm run build` and `npm run test:run` pass. |
||
|
|
b73ee491c8 |
fix: honour the client's settings option in idle compaction (#1555)
## Problem
A client can pass Claude Code settings in
`_meta.claudeCode.options.settings`, and the CLI gets them as
`--settings`. Idle compaction ignored them: it read only
`settingsManager.getSettings()`, the settings files. A client that sent
`{ idleCompaction: false }` still got `/compact` after about 54 idle
minutes, though the CLI had the setting off. The same went for
`autoCompactEnabled` and the `env` block (`DISABLE_COMPACT`,
`DISABLE_AUTO_COMPACT`, `CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS`). This
affected both ACP v1 and v2.
## Change
- `SettingsManager.getCompactionSettings(override)` resolves
`idleCompaction`, `autoCompactEnabled` and `env` in the CLI's order:
managed settings, then `--settings`, then the user/project/local files.
`env` merges entry by entry. An `undefined` value in the override does
not hide the files' value. This sits next to `getApiKeyHelper` and
`getModelSelectionSettings`, which already take the override the same
way.
- v1 (`src/v1/acp-agent.ts`) and v2 (`src/v2/reports.ts`, via
`ReportsSetup.configuredSettings`) pass the client's settings to it.
`IdleCompaction` itself is unchanged.
## Tests
- `settings-api-key-helper.test.ts`: managed > override > files for each
key and each `env` entry; an `undefined` override key keeps the files'
value.
- `v1/idle-compaction-agent.test.ts` and `v2/acp-v2.test.ts`: a session
started with `settings: { idleCompaction: false }` gets no `/compact`.
Both fail without the wiring change.
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Erdős Péter <6430206+sodre90@users.noreply.github.com>
|
||
|
|
626015bf81 |
fix: replay the messages before each compaction on ACP v1 and v2 (#1430)
Fixes #1297. After `/compact`, `session/load` replays from the summary of the last compaction only. `getSessionMessages` follows `parentUuid` links, and Claude Code writes each `compact_boundary` with `parentUuid: null`, so the chain ends there. The messages before it are still in the `.jsonl`, and the boundary still links the last one of them in `logicalParentUuid`; nothing follows that link. The spec requires the agent to "replay the entire conversation", and the replay test `replays a compaction as one completed compaction_update at its position` (`v2/acp-v2.test.ts`) already mocks a prompt before the summary, which the real SDK never returns. This now happens without `/compact` too: since #1522, idle sessions compact on their own. It reproduces in Zed as well ([zed-industries/zed#66523](https://github.com/zed-industries/zed/issues/66523#issuecomment-6644375981)). - When the chain starts at a compact summary, replay reads the transcript again through `readHistoryAcrossCompactions` (`resumed-session.ts`). It loads every record with `loadFullSessionHistory`, which moves from `fork-session.ts` (where it reads the inactive branches for the same reason) to sit beside `findTranscript`. It links each boundary to its `logicalParentUuid` and passes the records back to `getSessionMessages` as a `sessionStore`. The SDK's own parsing (queued commands, meta messages, deliveries) is unchanged. - The boundary's `preservedSegment` and `preservedMessages` are dropped. With them, `getSessionMessages` moves the messages a compaction kept to after the summary, and the relinked boundary then leads from them back to the summary, so the chain loops and ends there. Without them, the kept messages replay once, where they were said. - The v1 agent's `session/load` and the v2 agent's replay both read the history this way. - A session without a compaction reads its transcript once, as before. Resume, fork and rewind still use the active chain. If the second read fails, replay logs it and sends the active chain, as it does today. ## Testing - `npm run check` and `npm run build` pass. `npm run test:run` passes except for 2 tests that fail the same way on `main` on this machine: `v1/acp-scenarios.test.ts > normalize > replaces only the version of this adapter and the path of this Node binary` (Node at `/usr/bin/node`), and `v2/v2.test.ts > lists bypass before auto in ExitPlanMode's options after plan mode left bypass` (this machine runs as root, where bypass is not offered). - New: `compacted session replay` (`v1/acp-agent.test.ts`) replays a real transcript file with one `/compact` between two prompts to a client that declares `session.compaction`. It fails on `main` without this change: the replay has no "hi" and no "Hello". - New: `replays the messages before a compaction from the transcript` (`v2/v2.test.ts`) resumes the same transcript on v2 with `replayFrom: start`. Without the change, the replay has no user message before the compaction. - New: `readHistoryAcrossCompactions` (`resumed-session.test.ts`) covers two compactions, a compaction with both forms of preserved metadata as Claude Code 2.1.293 writes them, and a session without a compaction. - On 10 local transcripts with 1 to 10 compactions, no message replays twice. 9 of them now replay from the first prompt with one summary per compaction, for example 414 → 1925 messages and 909 → 3693. The tenth has many records written a second time, and replays 5 of its 10 compactions (1428 → 7526 messages). The second read took 4–220 ms, and 745 ms for that 33,000-line transcript. - Rechecked with 4 transcripts written by Claude Code 2.1.280 to 2.1.318, with 1, 2, 3 and 6 compactions. `session/load` sends one `compaction_update` per compaction, where `main` sends only the last one; for the 6-compaction transcript `main` replays nothing before the last summary. No summary replays as a user message and no tool call replays twice. --------- Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com> |
||
|
|
36cb674589 |
fix: include inline model settings in the model picker (#1488)
When `CLAUDE_MODEL_CONFIG` or caller-provided SDK settings list a model that the SDK omits, include those settings when rebuilding the picker. Configured models such as Bedrock Haiku versions appear with their provider override IDs and can be selected in both ACP v1 and v2. Merge file and inline model lists with the SDK's array semantics, preserve managed allowlists and override precedence, and keep the existing shallow caller-over-environment merge. The refreshed change preserves upstream's resumed-model alias handling and uses the v2 setup path introduced on current main. This repairs explicitly configured models; it does not discover an older model from a gateway listing or change the SDK's family defaults. Validation on upstream main `a1d83c60310f33cbc3cd78f6b6133278e754efd5` with SDK 0.3.321: - TypeScript compilation, ESLint and Prettier checks passed using their installed executables (the host npm script launcher exits 194). - 181 focused tests passed, including managed policy and environment, caller-object and caller-file settings; both v1 and v2 select the configured provider ID. - The three new v2 regression cases fail on unchanged main because the configured models are missing. - Full suite passed: 3,952 tests, 33 skipped. The first sandboxed run could not open a local socket or receive filesystem watcher events; the unchanged candidate passed with host test execution. - No real model inference or live provider calls were used. --------- Co-authored-by: Codex <codex@openai.com> Co-authored-by: Aleksandr Slapoguzov <aleksandr.slapoguzov@jetbrains.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f5d79167a6 |
fix: keep a v2 session idle when the model changes between turns (#1586)
On ACP v2, changing the model of an idle session left the session
reporting `running` with no work in progress, and it never reported
`idle` again until some later turn ended. A client that refuses prompts
while the session is `running` could then never send the prompt that
would end it.
## Cause
After `setModel`, Claude Code echoes a user message that carries only
the local command's output:
```json
{
"type": "user",
"message": {
"role": "user",
"content": "<local-command-stdout>Set model to `haiku (claude-haiku-5-5)`</local-command-stdout>"
},
"parent_tool_use_id": null,
"uuid": "…",
"isReplay": true
}
```
The v2 session read every top-level echo as a prompt that Claude Code
took in, and opened a turn for it. A model change runs no turn: no
`command_lifecycle` frame, `result` or `session_state_changed` follows
the echo, so nothing closed that turn.
Captured against the bundled CLI (adapter 0.104.1): `state_update
running` went out 18 ms after `session/set_config_option`, and no `idle`
followed in the next 75 seconds.
## Fix
An echo whose content is only local command output, and whose uuid is
not one of the session's pending prompts, no longer opens a turn. The
check is `isLocalCommandOutput` in `message-filters.ts`, next to the
helpers that already recognize these markers.
The pending-prompt exception keeps a client's own prompt working with a
CLI that sends no `command_lifecycle` frames, where the echo is the only
sign that the prompt was inserted.
## Scope
- v2 only. The v1 core takes its state from `session_state_changed` and
does not open turns at echoes.
- I captured a model change. Other local commands that Claude Code runs
outside a turn and echoes the same way are covered by the same check,
but I did not capture them.
## Testing
- New case in `src/tests/v2/v2.test.ts` sends the echo as the CLI sent
it. Without the fix it fails with `expected [ 'running' ] to deeply
equal []`.
- Reran the capture against the bundled CLI with the fix: no `running`
after either model change, and the next prompt ran `running` then
`idle`.
- `npm run check` and `npm run test:run` pass.
|
||
|
|
a4a8534c23 |
feat: serve session/set_title on ACP v1 and v2 (#1585)
Implements the [`session/set_title` RFD](https://agentclientprotocol.com/rfds/session-set-title) on ACP v1 and v2. ## What clients get - v1 advertises `agentCapabilities.sessionCapabilities.title: {}` and v2 advertises `capabilities.session.title: {}`. Both serve `session/set_title` for every client. - The session does not need to be loaded. A session that runs on this connection is renamed through its CLI (`rename_session`). Any other session gets the `custom-title` and `agent-name` records in every copy of its transcript, plus the `custom-title.json` sidecar, as the CLI's `/rename` writes them. - The title is normalized the way `_session/rename` normalizes it: whitespace collapsed, cut to the CLI's 200 characters. The response carries the stored title, as that client's `session/list` shows it. On v1 an AIR client gets it without the `[archived] ` prefix. An archived session stays archived. - A loaded session publishes `session_info_update { title }`. A title generation in flight finishes first, and no generated title replaces the client's. - Errors: a title that is empty after normalization returns `-32602`. A session that is not loaded and has no transcript returns `-32002`. On v2, a subagent session is refused, as close and delete refuse it. ## Structure - The transcript writer behind `_session/rename` lived in `src/v1/session-index/`, which v2 cannot import. Everything there except `connection.ts` moves to the shared `src/session-index/` with no change beyond import paths. `connection.ts` becomes `src/v1/session-index.ts`. - `src/session-index/rename.ts` holds the rename logic that used to be in `SessionIndexConnection.rename`/`retitle`. `_session/rename` and `session/set_title` on both versions call it. - Clients without `sessionIndex` (and every v2 client) use a `SessionIndexService` with no list and no watcher (`transcriptWriter`). Their `session/delete` now runs inside that service's per-session `exclusive()`, so a delete cannot race a rename that is still appending records. - If a restart or resume starts a new query of a v2 session while a rename runs, v2 renames through the new query too. Its CLI could otherwise hold the old title and write it back. This doesn't use the SDK's top-level `renameSession()`. It writes only a `custom-title` record to the first copy of the transcript it finds, with no `agent-name` and no sidecar, so a session renamed earlier through `/rename` would keep its old name in the CLI and in AIR. ## Tests - v1 (`session-index.test.ts`): a stored session for a client without the session index; an archived session for three clients (AIR with `sessionIndex`, AIR without it, a client that is not AIR); unknown session and empty title. - v2 (`acp-v2.test.ts`): an active session through its CLI with the published update; a stored session's records; the rename that a resume overtakes; unknown session and empty title. - Golden files: only the `initialize` capability line changes. `compare.ts` now allows the `title` capability against the origin/main baseline. - Not run: `npm run test:integration` against the real CLI. The delete/rename ordering has no test, since it is a timing race. |
||
|
|
6bc2e058cb |
feat: serve session/status on v1 and v2 (#1584)
Implements the unstable [Session status RFD](https://agentclientprotocol.com/rfds/session-status) on both agents, and advertises `status: {}` in the v1 `sessionCapabilities` and the v2 `capabilities.session`. ## What it answers | Session | v1 | v2 | | --- | --- | --- | | Held by this connection | `open` | `open` with `state` | | Not held, non-empty transcript stored | `saved` | `saved` | | Not held, no transcript | `-32002` with `data.sessionId` | same | | Subagent session | `-32002` | `-32602`, as resume, close, fork and delete of one | - The request starts no query, sends no update, and waits for no resume, restart or close. - On v2, `state` is the foreground state as a `state_update` snapshot holds it: with the session's `background` list and without a stop reason. `V2Background.snapshot` builds it without marking the list as reported, so a status request does not change which later `state_update`s go out. - `saved` or unknown is decided in the new shared `src/session-status.ts`: a non-empty `<sessionId>.jsonl` in any project directory. The request names no `cwd`, so a resume with another `cwd` can still fail with `-32002`. A directory that cannot be read for another reason than being absent fails the request and does not answer `-32002`. ## Behaviour to review - **A v1 session whose query ended reads `open`.** The connection still holds it, and a `session/load` with the same params returns it as it is, so `saved` would point the client at a resume that revives nothing. Its prompts fail with the "session ended" error. v2 drops such a session and answers from its transcript. - **A session that was never prompted is unknown once it is closed.** Claude Code writes the transcript at the first prompt, and a resume of it fails with `-32002` too. - **A subagent session does not advertise `status`,** so v2 refuses the request like its other requests, and v1 does not know the id. - **`open` is per connection.** Each connection has its own agent and its own Claude Code processes. ## Not done - The lookup does one `readdir` and one `stat` per project directory for each probe of a session that is not held. Sharing the v1 session index's cached scan is a follow-up. - Not run against the real CLI (`npm run test:integration`). ## Other changes - `isSessionId` and `projectsRoot` moved from `src/v1/session-index/project-dirs.ts` to `src/paths.ts`, so the shared module can use them. - The 40 v2 and 2 AIR scenario golden files gain the capability in `initialize`, and the origin/main comparison allows it, next to `systemPrompt` and `auth.status`. - The README lists `session/status` in the v2 surface. |
||
|
|
99cc1f3245 |
fix: report Claude Code's API error message as a notice in v2 (#1582)
## Problem When an API request fails, Claude Code writes its own assistant message into the conversation, e.g. "API Error: Connection lost mid-response. The response above may be incomplete." The SDK marks it with `error` on a `<synthetic>` message. The v2 agent forwarded that message as an ordinary `agent_message_chunk`, so clients showed it as Claude's own prose. Right after a thinking block it reads as if Claude wrote it, and a client cannot tell it apart without matching the string. ## Change The v2 agent no longer sends that message as Claude's text: - When the turn fails, the idle's `error` already carries the failure, so nothing else is sent. - Otherwise the text goes out as a `notice` with severity `error`, once, before the turn's idle. A result that repeats the same text is not shown again. Claude Code's sign-in message is handled as before. ## Not changed - v1 keeps its current behaviour: clients with the session-failure extension already drop this text, and others keep the transcript line. - `session/load` replay still shows these messages as text. `getSessionMessages` strips the `error` marker, so replay cannot identify them without matching prose. ## Testing - New v2 cases: a notice when the turn ends normally, one notice when the result repeats the text, and no notice when the idle fails. They fail without the change. - `npm run check` and `npm run test:run` pass. |
||
|
|
bce309f482 |
refactor: move the v1 client connection out of acp-agent.ts (#1576)
Moves `ClientConnection` and `ChangedMetaClient` verbatim from `src/v1/acp-agent.ts` into `src/v1/client-connection.ts`. No behaviour change. - Both classes gain `export`; nothing else in them changes. - `client-connection.ts` imports nothing from `acp-agent.ts`. - The `acp-client.ts` comment that names where `ClientConnection` lives now points to the new file. - Emitted JS differs from `main` only in `dist/v1/acp-agent.js` (the import and the two classes) plus the new `dist/v1/client-connection.js`. Review with `git diff --color-moved` to see the move. |
||
|
|
7d95d30ec3 |
fix: distinguish Ultra from Max in auth status (#1574)
## Summary - Read the optional `planTier` field from the existing `claude auth status --json` probe. Display Max subscriptions with `max_20x` as **Claude Ultra**, while `max_5x` remains **Claude Max**. - Fall back to the existing subscription label when the tier is missing, unknown, or malformed. Preserve auth-source precedence and raw plan metadata. - Keep the CLI-derived label when a session SDK account read reports the same identity and plan without a tier. A fresh CLI probe can still replace the label, including when its tier is absent. - Cover ACP v1 and v2 notifications and responses, and document the display behavior. The SDK AccountInfo omits the plan tier, but the bundled CLI already emits it in auth-status JSON. This uses that CLI-only field as best-effort display metadata, without additional probes, direct credential-file reads, or entitlement checks. ## Validation - `npm run build` - `npm run check` - `npm run test:run` — 4,002 passed, 33 skipped - Targeted v1 auth-status and v2 protocol tests — 341 passed - Local bundled CLI smoke check: `subscriptionType: "max"`, `planTier: "max_20x"` maps to `Claude Ultra` - Live model integration tests were not run. |
||
|
|
3d6eb7b5b6 |
fix: carry the auth detail in the ACP account label (#1573)
## Summary The ACP `auth/status` account (`label`, `active`, `name`, `email`, `organization`) has no field for the identity's `detail`: an API key's source, a gateway's host, or "OAuth token". It was only in `_meta.claudeCode.detail`, so a client that shows the standard fields could not tell which API key or gateway is in use. `toAcpAuthStatus` now appends the detail to the account's `label` in parentheses: - `Anthropic API key (ANTHROPIC_API_KEY)` - `Custom model gateway (gw.example.com)` - `Claude subscription (OAuth token)` An identity without a detail keeps its label as it was (`Claude Max`). ## Unchanged - `_meta.claudeCode` still carries `kind`, `detail` and `plan` on their own. - The `_auth/status_update` extension payload (`label` and `detail` as separate fields). - The mapping is shared, so v1 and v2 report the same label in `auth/status` and `auth/status_update`. ## Testing - `src/tests/v1/auth-status.test.ts` expectations updated for the API key and gateway labels. - `npm run test:run`, `npm run check` and `npm run build` pass. |
||
|
|
c09543d85a |
fix: offer Anthropic Console login in remote environments (#1572)
## Problem In a remote environment (`SSH_CONNECTION`, `SSH_CLIENT`, `SSH_TTY`, `NO_BROWSER` or `CLAUDE_CODE_REMOTE` set), `initialize` advertised only `claude-login`, the TUI `claude /login`. A user with API billing had no Anthropic Console option. With `--hide-claude-auth`, the list was empty and there was no way to log in at all. There is a second bug. A `_meta["terminal-auth"]` command reruns the agent with its own flags, for example `--hide-claude-auth --cli auth login --console`. The `--cli` passthrough removed only `--cli`, so the Claude CLI failed with `unknown option '--hide-claude-auth'`. ## Fix - `terminalAuthMethods` (`src/auth-methods.ts`) now offers `console-login` in remote environments too. Since Claude Code 2.1.126, `auth login` accepts the OAuth code pasted into the terminal when the localhost callback can't be reached. The subscription method stays as `claude-login` on a remote host and `claude-ai-login` locally. `--hide-claude-auth` hides it in both cases. The change is in the shared module, so it applies to v1 and v2. - New `cliArgs()` (`src/agent-command.ts`) removes `--cli` and `--hide-claude-auth` before the arguments go to the Claude CLI. ## Tests - `v1/authorization.test.ts`: an `it.each` over the five remote env vars × typed/`_meta` terminal auth × with/without `--hide-claude-auth`. - `v2/acp-v2.test.ts`: a remote host with `--hide-claude-auth` gets exactly one `console-login` terminal method. - `agent-command.test.ts`: `cliArgs` under Node, a compiled binary, and `--cli --version`. Manually checked in a JetBrains IDE through a local launch overlay with `SSH_CONNECTION` set and `--hide-claude-auth`. Only Anthropic Console was offered, the terminal login completed, and the chat worked afterwards. |
||
|
|
d202748976 |
feat: implement the ACP auth/status RFD next to _auth/status_update (#1570)
Implements the [auth status RFD](https://agentclientprotocol.com/rfds/get-auth-state) on ACP v1 and v2. The `_auth/status_update` extension is unchanged and still sent, so clients that read it keep working. ## What changes for a client - `initialize` advertises `auth.status: {}` (v1 under `agentCapabilities`, v2 under `capabilities`), next to the existing `_meta.authStatus` marker. - `auth/status_update` is sent whenever the identity changes, right after `_auth/status_update`. Each notification is deduplicated on its own payload. - `auth/status` answers with the identity the connection holds. - If an identity is known, it answers at once and starts a `claude auth status` read in the background. A change that read finds arrives as `auth/status_update`. - If none is known yet (a call right after `initialize`), it waits for the probe `initialize` started, bounded by the existing 5 s timeout, and answers `unknown` if that publishes nothing. ## Mapping `toAcpAuthStatus` in `src/auth-status.ts` maps the existing identity: | Identity | `state` | `accounts` | | --- | --- | --- | | not read yet | `unknown` | none | | `kind: "none"` | `unauthenticated` | none | | any other kind | `authenticated` | one entry, `active: true`, with `label`, `email`, `organization` | The RFD account has no field for the credential type, so `kind`, `detail` and `plan` go under the account's `_meta.claudeCode`. ## Notes for review - `state` describes the agent's own login. It does not count a `providers/set` route, and `--hide-claude-auth` does not change it: a subscription login is reported as `authenticated` even though the subscription guard refuses `session/new` for it. The legacy notification behaves the same way. - `auth/status_update` goes to every client. The RFD has no client capability to gate on. - The background read from `auth/status` is an ordinary probe, so under `--hide-claude-auth` it can mark a session for recreation when the account kind changed, as the probe at the next prompt would. - Probes are now numbered in start order. One that lands after a later-started probe has published is discarded, so an older read cannot overwrite a newer one. - `AcpClient` gained `authStatusUpdate`, so two test doubles needed a stub. - 42 scenario golden files change by one line each (the new capability in `initialize`), and the origin/main baseline comparison allows `auth.status`. ## Testing `npm run check`, `npm run build` and `npm run test:run` pass. New tests cover the mapping, the capability, `auth/status` and `auth/status_update` on v1 and v2, a gateway login, a first-probe timeout, out-of-order probes and the dedupe. |
||
|
|
966aee0f04 |
refactor: move the test-only tool render and metadata wrappers into src/tests/ (#1569)
`toolInfoFromToolUse` and `toolUpdateFromToolResult` in `src/tools.ts` and `isLocalCommandMetadata` in `src/message-filters.ts` were public `lib.ts` exports. #1566 removed them from `lib.ts`, so now only tests call them. They're thin wrappers: the first two build an `AcpToolCallRenderer` for one tool use or result, and the third checks that `stripLocalCommandMetadata` drops everything. They move unchanged to `src/tests/tool-render.ts`. `tools.test.ts`, `diff-patch.test.ts` and `v1/acp-agent.test.ts` import them from there. Production code renders through `AcpToolCallRenderer` directly, as before. Code changes only: no test changes and no behavior change. A scan of `src/` for exports finds no export that nothing uses. The only remaining test-only exports are the deliberate test hooks `clearDiffMemo` (`diff.ts`) and `sleep` (`utils.ts`). Test results: - `npx tsc --noEmit`, `npm run check`, `npm run build`: pass - `npx vitest run`: 3939 passed / 33 skipped |
||
|
|
66adc1a4c9 |
refactor: share the subagent replay's transcript scan between v1 and v2 (#1568)
`src/subagent-replay.ts` (v1, `RfdSubagentReplay`) and `src/v2/subagent-replay.ts` (`SubagentReplay`) each kept a copy of the transcript scan. The copies differed only in formatting and variable names. The scan does four things: - It registers each Agent or Task launch that has metadata as a child, and records a launch without metadata as omitted. - It records each SendMessage and SubagentHandback call. - It marks a failed hand-back. - It sets a foreground launch's end from its tool result. That scan, the `ReplayChild` and `SendMessageCall` types, the child's end state, and the `contentBlocks`, `inputOf`, `textOf` and `isDirectory` helpers move to `ReplayTranscript` in `subagent-replay.ts`. Each replay holds one and reads `children`, `sendMessages`, `handbacks` and `omitted` from it. What each replay sends stays where it was. v1 publishes asynchronously through `Publish`, and v2 sends synchronously through `SessionOutput`, with the v2-only rules: each child's end also goes on the child's own stream, and live children get their current state after the response. Sharing those parts would tie the two protocols together, so I left them separate. The scan only parses transcripts, so sharing it adds no dependency between v1 and v2. v2's `ReplayChild` is the shared type plus `output?: SessionOutput`. Its `launchEnd` was a narrower `"completed" | "failed" | "cancelled"`, which `endState` passed through `nativeSubagentState`. That call returns those three values unchanged, so `replayEndState` gives the same result in both versions. No behavior change: 128 lines added and 242 removed, with no test changes. Test results: - `npx tsc --noEmit`, `npm run check`, `npm run build`: pass - `npx vitest run`: 3939 passed / 33 skipped. The v1 and v2 replay scenario goldens are unchanged. - `RUN_INTEGRATION_TESTS=1 npx vitest run`: passes. One run hit the known load flake "absorbs the interrupt's lagged trailer after cancelling a held turn", which passes on its own. |
||
|
|
bc031f362c |
test: split src/tests/ into v1/, v2/ and shared tests (#1567)
`src/tests/` was flat, so it didn't follow the `src/v1/` / `src/v2/` layout. - **`src/tests/v1/`** holds the 37 tests that import `src/v1/`, plus `session-doubles.ts`, which builds v1 sessions. - **`src/tests/v2/`** holds the tests of `src/v2/` and `serve.ts`: `v2.test.ts`, `v2-integration.test.ts`, `acp-v2.test.ts` and `acp-scenarios-v2.test.ts`. - **The top level** keeps the tests of shared modules, plus `helpers.ts`, `setup-no-network.ts`, the scenario harness (`acp-scenarios/`) and `fixtures/`. A test goes to `v1/` when it imports the v1 agent, even if it is mostly about a shared module. For example, `providers.test.ts` and `fast-mode-config.test.ts` drive `ClaudeAcpAgent` for some of their cases. Only paths change. Imports and `vi.mock`/`vi.importActual` specifiers gain one `../`. The `here`-relative paths to the goldens, the origin/main recordings, `package.json` and `fixtures/` were updated too. `AGENTS.md`, `docs/air-extensions.md` and the scenario files' doc comments now give the new paths. No test, golden file or recording changed. A script checked that every relative module specifier under `src/tests/` resolves, because a `vi.mock` path that doesn't resolve mocks nothing and raises no error. Test results: - `npx tsc --noEmit`, `npm run check`, `npm run build`: pass - `npx vitest run`: 3939 passed / 33 skipped, the same as before the move. - `RUN_INTEGRATION_TESTS=1 npx vitest run`: 3964 passed / 8 skipped. |
||
|
|
566e2652a9 |
refactor!: export only lib.ts, with serveAcp and the authStatus wire names (#1566)
Stacked on #1564. The package's `exports` map listed `"./*": "./*"`, so every file under `dist/` was public, and any file move could break a deep import. It now lists only `.` (`dist/lib.js`) and `./package.json`. **`lib.ts` exports:** - `serveAcp`, which serves ACP v1 and v2 on stdio through the SDK's protocol router. This is what `index.ts` runs. - The `AcpProtocols` and `Logger` types. - `AUTH_STATUS_UPDATE_METHOD` and the `authStatus` payload types, which a client needs to read the extension's traffic. **No longer exported:** - `ClaudeAcpAgent` - `runAcp`: v1 only and unused, replaced by `serveAcp`, and now deleted. - `toAcpNotifications`, `streamEventToAcpNotifications`, `isLocalCommandMetadata`, `stripLocalCommandMetadata` - `toolInfoFromToolUse`, `toolUpdateFromToolResult`, `toDisplayPath`, `planEntries` - `SettingsManager` - `nodeToWebReadable`, `nodeToWebWritable`, `Pushable`, `unreachable` - The types `NewSessionMeta`, `SDKMessageFilter`, `ToolUpdateMeta` and `ClaudePlanEntry`. `src/acp-agent.ts`, which re-exported `src/v1/acp-agent.ts` for the `dist/acp-agent.js` path, is gone. So are the re-exports in `src/v1/acp-agent.ts` that kept moved names on that path. Tests now import those names from their own modules. The ESLint boundary rules and `AGENTS.md` drop their `acp-agent.ts` exemptions. `release-please-config.json` sets `bump-minor-pre-major`, so the `!` bumps the minor version, the same as a `feat:`. BREAKING CHANGE: deep imports of `dist/` files fail with `ERR_PACKAGE_PATH_NOT_EXPORTED`, and `lib.ts` exports only `serveAcp`, `AUTH_STATUS_UPDATE_METHOD` and their types. Test results: - `npx tsc --noEmit`, `npm run check`, `npm run build`: pass - `npx vitest run`: 3939 passed / 33 skipped. `module-exports.test.ts` pins the `lib.ts` names and the `exports` map. - `node scripts/smoke-binary.mjs -- node dist/index.js`: pass - Resolving the package from its root: `import("@agentclientprotocol/claude-agent-acp")` gives `serveAcp` and `AUTH_STATUS_UPDATE_METHOD`, `…/dist/acp-agent.js` throws `ERR_PACKAGE_PATH_NOT_EXPORTED`, and `…/package.json` resolves. |
||
|
|
f115b90161 |
refactor: move the v1 agent into src/v1 (#1562)
Stacked on #1561. Moves the modules that only the v1 agent uses into `src/v1/` with `git mv`, so the tree reads as `src/v1/` (v1 agent), `src/v2/` (v2 agent) and the rest of `src/` (shared). No behavior change. The v1-only set comes from the import graph: modules reachable from `acp-agent.ts` whose every non-test importer is itself v1-only (entry points `lib.ts`/`serve.ts` aside). Moved: - `acp-agent.ts`, `background-work.ts`, `goal-extension.ts`, `turn-events.ts` - `tool-calls/changed-meta-filter.ts` -> `v1/tool-calls/changed-meta-filter.ts` - `session-index/*` -> `v1/session-index/*`, except `session-index/explicit-title.ts`, which stays because `session-titles.ts` (shared with v2) imports it Stay shared: `clear-context-coordinator.ts`, `native-subagents.ts` and `acp-subagents.ts`. v2 and shared modules import all three. - `src/acp-agent.ts` is now `export * from "./v1/acp-agent.js"`. `dist/acp-agent.js` exports the same names, and `module-exports.test.ts` passes unchanged. `lib.ts` and `serve.ts` import `v1/acp-agent.js` directly. - Imports in `src/` and tests, path mentions in comments, and AGENTS.md are updated. Scripts, vitest config and docs referenced no moved path. - History: `git log --follow src/v1/acp-agent.ts` follows the move. Because `src/acp-agent.ts` still exists as the re-export, blame needs `git blame -C`. Test results: - `npx tsc --noEmit`, `npm run check`, `npm run build`: pass - `npx vitest run`: 3939 passed / 33 skipped, goldens unchanged - `node scripts/build-binary.mjs && node scripts/smoke-binary.mjs`: pass Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
617d5351ed |
refactor: move the Claude Code to ACP mapping out of acp-agent.ts (#1561)
Stacked on #1560. The v2 agent in `src/v2/` imported the Claude Code to ACP mapping, shared types and a few helpers from `acp-agent.ts` (17 imports), so it loaded the whole v1 agent module. This moves that code into modules that own it. `acp-agent.ts` imports it back and keeps re-exporting every name it exported before, so `dist/acp-agent.js` deep imports still work. What moved where: - `src/sdk-to-acp.ts`: `toAcpNotifications`, `streamEventToAcpNotifications`, `messageIdForGrouping`, `stampParentToolUseId`, the `ToolUseCache` / `StreamedToolInputCache` types and the streamed tool input scanner. - `src/message-filters.ts`: `stripLocalCommandMetadata`, `isLocalCommandMetadata`, `isSyntheticLoginMessage`, `isReplayHiddenMetaMessage`, `LOCAL_ONLY_COMMANDS` and the transcript marker scanner. - `src/prompt-to-claude.ts`: `promptToClaude`, `SUPPORTED_PROMPT_CONTENT_TYPES`. - `src/slash-commands.ts`: `getAvailableSlashCommands`. - `src/result-end.ts` (existing): `AUTONOMOUS_RESULT_ORIGINS`, `isAbortedResult`. - `src/logger.ts`, `src/acp-client.ts`, `src/new-session-meta.ts`: the `Logger`, `AcpClient`, `NewSessionMeta` and `SDKMessageFilter` types. About 1,360 lines leave `acp-agent.ts` (12,718 to 11,354). The code moved as-is. Only a few comments that named `acp-agent.ts` as the home of the shared code were reworded. `src/v2/`, `utils.ts`, `serve.ts`, `lib.ts` and `session-index/connection.ts` now import from the new modules. Tests import the moved code from the new modules too. None of the new modules imports `acp-agent.ts`. `grep -rn acp-agent src/v2` returns nothing, and a walk of the static imports in `dist/v2` reaches 119 modules, none of them `acp-agent.js`. No behavior change. The export pins in `src/tests/module-exports.test.ts` for `lib.js` and `acp-agent.js` pass unchanged. Test results: - `npx tsc --noEmit`, `npm run check` and `npm run build` pass. - `npx vitest run`: 3939 passed, 33 skipped. That is the 3935 before, plus the 4 provider tests now on the base branch. - Goldens unchanged. - `node scripts/build-binary.mjs && node scripts/smoke-binary.mjs` pass. Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
cb285cd285 |
feat: serve ACP v2 with a direct v2 agent and remove the v1 bridge (#1560)
## Summary ACP v2 was served by a bridge (`src/v2/agent.ts`) that converted each v2 request to v1 and ran it through `ClaudeAcpAgent`. Every v2 session therefore ran the v1 core's turn machinery: held prompts, echo matching, trailing-idle debt and the hold for background subagents. v2 asks for none of it. This PR serves v2 with its own agent in `src/v2/`, which follows Claude Code's own events. A prompt is answered when Claude Code takes it in, and `state_update` reports foreground work. Background subagents and tasks are listed in `background` and do not hold the turn. - **New v2 agent (`src/v2/`).** `V2Agent` and `V2Session` cover: - the prompt lifecycle and cancellation - subagents as child sessions, following the subagents RFD, plus background lists - replay, restarts for provider and route changes, auth and providers - elicitation, config options, `/remote-control`, and the `--hide-claude-auth` guard - **Bridge removed.** `ClaudeAcpAgent` serves ACP v1 only: its `v2` option, foreground/background hooks and `this.v2` branches are gone. v1 behavior is unchanged, and no v1 test expectation or golden file changed. The control flow of `acp-agent.ts` is simpler and the file is about 450 lines shorter. - **Shared modules.** Protocol-neutral logic moved out of `acp-agent.ts` into modules both versions use, including: - `session-model.ts`, `session-effort.ts`, `session-config-options.ts` and `session-env.ts` - `providers.ts`, `auth-status-reporter.ts` and `context-window.ts` - `result-end.ts`: one set of rules for reading a result in both cores - `prompt-suggestions.ts` and `mcp-servers.ts` - **JetBrains AIR on v2.** An AIR v2 client gets the AIR extensions that ACP v2 does not cover: - `recommendedValue` - the permission presentation - question custom answers - prompt suggestions - `agentFileChangeReport`, sent at the end of the turn, before its idle ACP v2 itself covers subagent sessions, background subagents, async tasks, plan files, diff patches, compaction, custom instructions and session failures. See "ACP v2" in `docs/air-extensions.md`. - **Ported from main.** Main's v2 changes made on the bridge are ported to the new agent, and main's v2 tests run against it: - workflow cards and phases (#1550, #1552) - task tool calls (#1556) - plan removal on reset (#1554) - subagent checklist plan ids (#1558) - the Fable alias (#1548) ## Behavior changes for v2 clients Each one follows the v2 draft spec, where the bridge did not: - **Background subagents.** They no longer hold the prompt open. They are listed in `background`, and their work is reported in their child session's state. - **`requires_action`.** While the root waits on foreground subagents, it reports `requires_action` only when every one of them waits on the user. - **Resume of an active session.** Its work keeps running, and the new MCP servers replace the old ones (`setMcpServers`). The bridge tore the session down. - **Cancel.** The idle after a cancel waits for Claude Code's aborted result and carries its usage. A stopped child's open tool calls end before its idle. - **`/remote-control`.** It runs as its own turn. Sent while Claude works, it waits for that work to end. - **Accepted URL elicitation.** It keeps the work `requires_action` until Claude Code reports the elicitation complete. - **Exited Claude Code.** A session whose Claude Code process exited stays active and answers -32603 with what happened, until it is resumed. - **Provider change.** `providers/set`/`disable` restarts idle sessions before it answers. A busy session restarts at its next turn boundary, and its prompts are not held meanwhile, which the provider RFD allows. ## Workarounds - **`/remote-control` waits outside the prompt queue.** The adapter's own `/clear` and plan prompts, which continue a turn in a fresh context, use their own push queue (`ownPushes`). They must not wait behind a `/remote-control` that waits for that same turn to end. - **Restart on additional directories.** A resume with other additional directories restarts the query at its next turn boundary. The SDK cannot change the directories of a running query. ## Rollback `CLAUDE_AGENT_ACP_PROTOCOL=v1` negotiates every client down to v1. The bridge and its `CLAUDE_AGENT_ACP_V2_CORE` switch are removed. ## Not served on v2 yet - AIR's tool call keys (`commandTitle`, `subagent`, `skill`, `mcp`), the session index, and API retry warnings. - A `replayFrom` cursor other than `start`, which is rejected with -32602, and steering, which is v1 only. ## Packaging - `lib.js` exports are unchanged. - `acp-agent.js` exports no name fewer than before. It adds `AUTONOMOUS_RESULT_ORIGINS`, `getAvailableSlashCommands` and `isAbortedResult`, which the v2 agent uses, and the export pin test is updated. - Internal `dist/v2/*` modules changed. They are reachable only through the `"./*"` export and are not documented. ## Testing - `npm run format:check`, `lint`, `build` and `test:run` pass: 3935 passed, 33 skipped. - `npm run test:integration` against the real CLI passes: 3960 passed, 8 skipped. This includes `src/tests/v2-integration.test.ts`. - The v2 golden scenarios (`acp-scenarios-v2.test.ts`) match without being regenerated. - The binary builds and `scripts/smoke-binary.mjs` passes. - Two tests unchanged from main can time out under heavy load: the session-index file watcher test and v1's "lagged trailer" test. Both pass on rerun. ## Follow-up - Split `src/` into v1, v2 and shared modules. - Move the SDK-to-ACP mapping that `src/v2/` still imports from `acp-agent.ts` into a shared module. - Add a lint rule that keeps the boundary. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
2f7e386f36 |
fix: give subagent checklists their own plan ID (#1558)
A subagent's TodoWrite checklist was published with the main checklist's `planId: "plan"`, so it replaced the main conversation's plan. This only happened on ACP v1 clients that advertise `plan` but not `subagents`. With native subagent sessions (every v2 client, AIR, and v1 with `subagents`), the checklist already goes to the child session, and that's unchanged. Changes: - In that case, a subagent's TodoWrite gets `planId: "plan:<parentToolUseId>"`, so it sits next to the main checklist (`todoWritePlanUpdate` in `src/plans.ts`). - An empty subagent TodoWrite sends `plan_removed` for its own plan ID, not `"plan"`. - `/clear` also removes the subagent checklist plans it published. - Nothing is removed when a subagent finishes. The plan operations RFD now says Agents remove cancelled plans, not finished ones. Not changed: - Task* tools keep `"plan"`. Claude Code shares one task list across all agents in a session, so a per-subagent ID would show the same list twice. - Clients without the `plan` capability can hold only one plan, so a subagent's checklist still replaces it there, as before. Tests cover v1 (main and subagent plans side by side, then `/clear`) and v2 (the checklist arrives as `"plan"` in the child session, and the root's plan is untouched). |
||
|
|
855447c7cd |
fix: update to @anthropic-ai/claude-agent-sdk 0.3.321 (#1557)
## Summary Updates `@anthropic-ai/claude-agent-sdk` from 0.3.319 to 0.3.321. It bundles Claude Code 2.1.321, which adds Fable 5.2 (`claude-fable-5-2`) as the new default `fable` model. Also bumps these dev dependencies: `@anthropic-ai/sdk` 1.6.0 → 1.7.0 and `@typescript-eslint/*` 8.72.1 → 8.72.2. `typescript` 7 is left for its own PR. ## Fable 5.2 Checked against the real CLI: - A new session lists `fable` → `claude-fable-5-2`, and `claude-fable-5-1` as its own row. - A resumed Fable 5.2 session lists `claude-fable-5-2` and no `fable` row, the same pattern 5.1 had. `aliasResumedModelRow` already handles any version, so the code is unchanged. Its doc comment and test fixture now describe the 2.1.321 lists. The fixture also covers a resumed Fable 5.1 session keeping `claude-fable-5-1`, because a newer Fable is listed. ## Changelog review - No SDK type changes that affect us. The only changes are doc text and a new optional `endedLoopWakeup` field in `sdk-tools.d.ts`. - Queued subagent reports in auto mode now share one model call, and all but the last end in a `num_turns: 0` placeholder result. These results carry `task-notification` or `peer` origins, which we already treat as placeholders. That handling dates from CLI 2.1.274, which did the same for background-task notifications. - Fixes we pick up: one `undefined` entry in `hooks` no longer drops every hook; a PreToolUse `ask` is no longer lost when another hook's `defer` finishes first; project skills and commands no longer go missing after entering a worktree; MCP OAuth no longer sends refresh tokens to another host after a redirect. ## Testing - `npm run build`, `npm run check`, `npm run test:run` - Real CLI: listed the models for a new session and for a resumed Fable 5.2 session Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
d098fb08d5 |
fix: report v2 background tasks without a tool call as their own tool call (#1556)
## Summary Background tasks that have no tool call or terminal on v2 now get a tool call of their own. Before, they showed up as a `_claude/task` vendor item. This follows the spec rule in agentclientprotocol/agent-client-protocol#2786. Examples: a workflow, or an MCP task that starts after its tool call already returned, or a task a hook started. - `BackgroundWork.adoptTasks` starts a tool call `task_<taskId>` for each such task. It uses the task description as title, `kind: "other"`, and `in_progress` or `paused`. A subagent's task goes on the subagent's stream. The message loop sends these calls before `sync`, so a call is always announced before the first `background` list names it. The same path covers the `state_update` snapshot on `session/resume`. - These calls are listed as `tool_call` items, so `tool_call/cancel` stops the task. When the task ends they get a final status like any other listed call (`completed`, `failed`, or `cancelled`). - The `_claude/task` item type is gone. `BackgroundItem` is now `terminal | tool_call | subagent`. - Ambient tasks such as the monitor, and tasks whose own tool call is still open, are unchanged. `adoptTasks` is synchronous and returns the notifications, so the loop only awaits when there is something to send. An extra await on every pass broke the steering-cancel timing in `acp-agent.test.ts`. ## Compatibility - ACP v1 clients, including JetBrains AIR's `_session/async_task/*` extension, are unaffected. Background lists, and therefore this path, only run when the v2 background sink exists and the session has `perTaskStopAffordance`. On v1, `adoptTasks` returns `[]`. - v2 clients that saw `_claude/task` items (v0.100.0 to v0.101.1) now see `task_<id>` tool calls instead. ## Tests `acp-v2.test.ts` replaces the `_claude/task` tests. New tests cover: - starting and listing a call for a task that starts after its tool call returned - cancelling such a task with `tool_call/cancel` - the call being sent before the `session/resume` snapshot - a subagent's task appearing on the subagent's stream `npm run check`, `build` and `test:run` pass. |
||
|
|
766fd142f6 |
feat: send the checklist as a plan operation and remove plans on reset (#1554)
## Summary Follow-up to the [plan operations RFD](https://agentclientprotocol.com/rfds/plan-operations). The adapter already sent approved ExitPlanMode plans as `markdown` plans to clients with the `plan` capability, but two parts of the RFD were unused: - **The checklist is now a plan operation on v1 too.** Clients with `plan` (every v2 client, and v1 clients that opt in) get the TodoWrite/Task checklist as `plan_update` with type `items` and the ID `plan`. Before, v1 clients with the capability still got an ID-less legacy `plan` next to the `plan-mode` plan. Clients without the capability still get the legacy `plan`. - **`plan_removed` is now sent.** - An empty checklist sends `plan_removed` for `plan`. v2 clients used to get a `plan_update` with no entries. - A conversation reset (`/clear`) removes the checklist and the old conversation's approved `plan-mode` plan. AIR keeps its approved plans, which are joined to their approval cards by ID (#1469). The new `src/plans.ts` builds the checklist updates. All three places that send the checklist use it, and so does the v2 conversion of a legacy `plan`, so v1 and v2 follow one rule. The reset sends `plan_removed` for `plan-mode` even when the session never published an approved plan. Tracking whether one was published would be state that a restart loses, so the reset would behave differently before and after one. `file` plans are left out. A `file://` URI only works for a client on the same machine, and v2 has no client filesystem. ## Tests - `tools.test.ts`: TodoWrite with and without todos, and a TaskCreate snapshot, for a v1 client with `plan`. - `acp-agent.test.ts`: the reset test now runs for a client without plan operations, a client with them, and AIR. - `acp-v2.test.ts`: v2 end to end, TodoWrite then `/clear`, and an empty legacy plan converting to `plan_removed`. |
||
|
|
176ae0634c |
feat: list workflow phases and agents on the workflow card (#1552)
## Summary Show the phases and agents of a running workflow on its ACP v2 tool card, not only the current activity and totals. Claude Code attaches a `workflow_progress` snapshot to some of a workflow's `task_progress` events: every phase, and every agent with its label, phase, state, tokens, tool calls, duration and last tool. #1550 deliberately left it out. This change renders it under the existing card body: ``` **Count** - two: done · 14,629 tokens · 0 tool calls · 1s - three: running · Bash: echo 3 **Sum** - not started ``` ### Why an untyped field The SDK (0.3.319) does not type `workflow_progress`. Workflow agents also don't stream their messages through the root query, so this snapshot is the only per-agent data the adapter gets. `src/workflow-progress.ts` reads the shape the CLI sends and skips entries that don't match it. If the CLI changes the format, the card loses the list but still renders. The right long-term fix is for the SDK to type this field, or to stream workflow agents with a parent id so each one can become a child session. ### Behavior - The snapshot is a full list, and the CLI leaves it out of some reports. A report without one keeps the last list. - A running agent shows its last tool (e.g. `Bash: echo 3`). A finished one shows tokens, tool calls and duration. - Agents with no known phase are listed first. A phase with no agent yet shows `not started`. A retry shows its attempt. A state other than `start`/`done` is shown as the CLI sends it. - The existing duplicate suppression and the `session/resume` snapshot cover the list too. - AIR `async_task_progress` is unchanged. Like the rest of the card progress from #1550, this only reaches v2 clients that get workflow progress on the card. Result previews, models and prompt previews are left out to keep the card short. ## Validation - Live probe on Claude Code 2.1.319 (SDK `query()`, two-phase three-agent workflow, permissions limited to `Workflow` and one `echo 3`). It captured the snapshot shape used in the test doubles. Every captured snapshot rendered through the built module as shown above. This checked the protocol output, not the Delta UI. - New v2 scenario built from the captured payload, plus unit cases in `src/tests/workflow-progress.test.ts`. - `npm run check`, `npm run build`, `npm run test:run` (3,691 passed, 30 skipped). |
||
|
|
30600ba348 |
fix: render workflow tool cards and progress correctly in ACP v2 (#1550)
## Summary Fix workflow tool cards that remain pending indefinitely, and surface workflow progress through standard ACP v2 updates. ### Prevent orphan preview cards Claude Code invokes Edit/Write preview hooks for workflow agents without streaming those tools' uses and results through the root query. Publishing a card from the hook alone leaves a pending call with no completion path. Stage early preview patches until the stream or a permission request publishes the actual tool call. Keep patches pinned against later input refinements, bound unclaimed previews, and clear them on teardown. A call's own content takes precedence over a staged preview. ### Show workflow progress in place Forward the SDK's typed task-progress fields to the existing workflow card: current activity, summary, last activity, token count, tool-call count, and elapsed time. Preserve progress arriving before the launch result, suppress duplicate display updates, and include the latest progress on session resume. Progress does not change paused/running state or reopen completed calls. This reads no workflow transcripts, journals, or metadata files and introduces no ACP extension. The CLI also emits an untyped `workflow_progress` snapshot; consuming that richer phase/agent data is deliberately left out of this change. Existing v1 AIR progress uses the same field normalization as before. ## Validation - Regression reproduced before the orphan-preview fix. - Nine relevant Vitest suites: **1,972 passed, 10 skipped**, including v1/v2 scenarios, preview ordering, progress-before-launch, idle and paused updates, duplicate suppression, resume snapshots, and late progress after completion. - `npm run check` - `npm run build` - `git diff --check` - Live Claude Code **2.1.319** probes: captured SDK workflow events and ran a real workflow through the updated ACP v2 adapter. One card received two progress updates and then reached `completed`. This verified protocol updates, not the Delta UI. Rebased onto current `main` and reran the automated checks before opening the PR. |
||
|
|
dd9eed4e46 |
test: pin runtime exports of lib.js and acp-agent.js (#1546)
Adds a test that pins the runtime export names of `lib.js` and
`acp-agent.js`.
`acp-agent.ts` is about to be split into smaller modules in a series of
pure-move PRs. Each move keeps the old exports as re-exports, because
`"exports": { "./*": "./*" }` lets consumers deep-import
`dist/acp-agent.js`. tsc only catches a missing export that something in
the repo imports; this test catches the rest. The list should change
only on purpose.
|
||
|
|
4cceb06b31 |
fix: list a resumed Fable 5.1 session's model as fable (#1548)
## Problem The Claude Code CLI gives Fable 5.1 a different option value depending on how the session starts: - **New session:** listed as `fable` (`resolvedModel: "claude-fable-5-1"`). - **Resumed session whose transcript last ran Fable 5.1:** the same row is listed as `claude-fable-5-1`, and there is no `fable` row. Reproduced with CLI 2.1.310. Other families don't do this: a session resumed on `claude-opus-5-5` still lists `opus`. Clients store the option value as the session's choice. A choice saved in a new session (`fable`) is not among the options of a resumed one, and the reverse. A client that checks its saved value against the options then refuses to apply it, and the model choice keeps flipping between the two ids. ## Fix `aliasResumedModelRow` in `session-model.ts` gives the resumed model's row its family alias back. It changes a row only when: - the row's value is the model read from the resumed transcript, equals its own `resolvedModel` and has no context hint, - no row already has the alias, and - no newer version of the family is listed, since the alias runs the family's newest model. `createSession` applies it before the allowlist and model selection, so new, resumed and loaded sessions over ACP v1 and v2 all get the same list. This is a workaround for the CLI listing the row under a different value. The CLI also lists rows this way when `ANTHROPIC_MODEL` or `settings.model` pins `claude-fable-5-1`. Those rows keep the pinned id on purpose: relabelling them would turn the user's pinned version into the alias, which moves to a newer Fable when one ships. ## Tests - `acp-agent-settings.test.ts`: a session resumed on `claude-fable-5-1` offers `fable` as the current value and option, with no `setModel` call. Fails without the fix. - `model-resolution.test.ts`: unit cases for the relabel and for each case that keeps the list as it is. - Checked end to end with the real CLI: a Fable turn, then `session/load`, reports `fable`. |
||
|
|
a58293f4c9 |
fix: update to @anthropic-ai/claude-agent-sdk 0.3.319 (#1545)
Updates the Claude Agent SDK from 0.3.318 to 0.3.319, which bundles Claude Code 2.1.319. Also updates `@anthropic-ai/sdk` (1.4.0 → 1.6.0) and the transitive dependencies in the lockfile (`@babel/runtime`, `@eslint-community/regexpp`, `@rolldown/pluginutils`, `hono`, `jose`, `vite`). ## Reviewed, no change needed - **`mcp_task` background tasks:** SDK sessions get these only with `CLAUDE_AUTO_BACKGROUND_TASKS=1` and streaming input. The adapter does not set that variable, so they don't occur by default. If a user sets it, the Async Tasks panel shows the raw `mcp_task` type, and the `resource_links` on the task notification are not forwarded. Left as a possible follow-up. - **`deleteSession()` rejects with `ENOTEMPTY`** when the session folder is only partly removed. `session/delete` already rethrows any error other than "not found". - **`session_writes_lost`** is now sent only when the CLI exits 0. The adapter only logs it. - **`permissions.denyInsteadOfPrompting`:** with it set, the CLI denies instead of calling `canUseTool`. That is the setting's intent. - **`mcp_call.check_permissions`, `set_has_subscribers`, `disableWindowsBashCommandSplit`:** the adapter does not use these. - The comments on the `getSettings` and `unanswered` type workarounds now name 0.3.319; both are still missing from the SDK's types. ## Not updated TypeScript stays on 6.0.3: typescript-eslint 8.72.1 requires `typescript <6.1.0`, so 7.0.2 has to wait for a typescript-eslint release that supports it. ## Testing `npm run build`, `npm run check` and `npm run test:run` pass. `npm run test:integration` was not run. |
||
|
|
3b22586cf6 |
fix(test): assert the blockquoted MCP startup transcript line (#1543)
`#1362` moved every transcript fallback for clients without `session.notices` into its own blockquote paragraph (`\n\n> …\n\n`) and updated the existing assertions, but missed the MCP server startup one. `main` has been red since that merge: - The `Build` job on `47b64c0` fails with an annotation at `src/tests/acp-agent.test.ts` line 13860: ``` AssertionError: expected '\n\n> **MCP server "client-fs" failed…' to be '**MCP server "client-fs" failed to st…' ``` - The neighbouring fallback tests already assert the blockquote form, e.g. `"\n\n> **Warning:** hook says no\n\n"` and the two-line `Stop says:` message. This updates the expected string to the blockquoted text; product code is untouched. ## Validation - `npx vitest run src/tests/acp-agent.test.ts -t "falls back to a transcript line without the notices capability"` → 1 passed - `npx vitest run src/tests/acp-agent.test.ts` → 759 passed | 9 skipped - `npm run check` → passes |
||
|
|
47b64c04de |
fix: start transcript notices on their own line for clients without session.notices (#1362)
## What When a client does not advertise `session.notices`, a notice falls back to an `agent_message_chunk` like `**Notice:** …`. Clients render consecutive chunks as one markdown stream, so the fallback is glued to Claude's last sentence. In Zed 1.24 a Stop hook's `systemMessage` shows up as: ``` …推上去。**Notice:** Stop says: token: 3 calls Stop says: hand off now ``` The fallback is now its own blockquote paragraph, with blank lines on both sides: ``` …推上去。 > **Notice:** Stop says: token: 3 calls > hand off now ``` ## Behavior - `noticeOrTranscriptUpdate` wraps the transcript text with `transcriptBlock`: a leading blank line so it starts on a new line, `> ` on every line, and a trailing blank line so Claude's next chunk does not continue the quote. This covers every transcript fallback (hook notices, "Task stopped by user", "Fast mode turned off", "Auto mode unavailable"). - Claude Code prefixes every line of a hook's `systemMessage` with `<HookEvent> says: `. The informational fallback keeps that prefix on the first line only, and only when every line repeats the same prefix (`collapseHookSpeaker`). - Clients that advertise `session.notices` get the same `notice` update as before. - `Turn.noticeTexts` still records `message.content` unchanged, so the hook-blocked result de-duplication keeps working. ## Tests - New unit tests for `transcriptBlock` and `collapseHookSpeaker`, plus an end-to-end test for a two-line `notice`-level hook message. - Updated the existing transcript fallback assertions to the blockquote form. - `npm run check`, `npm run build` and `npm run test:run` pass (3111 tests). Co-authored-by: alex <hongchang.wan@longbridge-inc.com> |
||
|
|
1ee2422934 |
fix: keep autonomous turn tasks open across owed trailing idles (#1455)
When Claude Code starts a new autonomous cycle before the previous
cycle's trailing idle arrives, that late idle completes the new cycle's
AIR async task while Claude is still working. The next output then
creates a second task for the same cycle.
This change completes the autonomous task inside the existing cycle-end
branch, which already checks that `owedTrailingIdles` is zero. An owed
idle is now only consumed by the existing accounting and leaves the
current task open. The task still completes at its own terminal result
or at an unowed idle, so the task and the cycle end on the same
decision.
It adds a scripted SDK-stream regression for two consecutive autonomous
cycles with no running transition between them. The test checks that the
late idle leaves the second task open and that later output stays on the
same task, for a cycle that ends at a result and for one that ends at an
unowed idle. Both cases fail before the fix and pass after it.
Fixes #1453.
Validation, rebased on 0.98.0 (
|
||
|
|
7d1ef76c1d |
fix: report setup-token logins as logged in (#1376)
Fixes #1295 With a `claude setup-token` login (`CLAUDE_CODE_OAUTH_TOKEN`), `claude auth status --json` only returns `{"loggedIn": true, "authMethod": "oauth_token", "apiProvider": "firstParty"}`. `fromCliStatus()` handed that to `mapAuthFields()`, which has no plan or key fields to go on and fell back to "Not logged in". Clients then showed a working session as logged out. This maps that exact state to `kind: "account"` with the label suggested in the issue ("Claude subscription", detail "OAuth token"). Everything else still goes through `mapAuthFields()`. Added a case to `auth-status.test.ts` that fails without the change. `npm run check`, `npm run build` and `npm run test:run` pass. |
||
|
|
aa0c1234b5 |
fix: replay a compaction summary by its flag, not its framing (#1536)
Fixes #1535 A compaction summary on replay was only sent as a `compaction_update` if its text also parsed, so a change in Claude Code's framing turned it into a `user_message_chunk`, shown as text the user typed. That is what happens to summaries from Claude Code 2.1.312 and 2.1.316. `isCompactSummary` now decides on its own. A summary whose framing isn't recognized is sent as a completed `compaction_update` without summary text. This reverses the choice to keep such text visible: the model-facing framing isn't kept as a summary either way, and showing it as the user's words is the worse failure. `PERSISTED_PREAMBLE` also allows content before the `Summary:` line, so current summaries keep their text. All 31 summaries in my own transcripts now parse, and a real `session/load` on one from 2.1.316 sends a `compaction_update` with its text and no summary `user_message_chunk`. |
||
|
|
54392e144d |
fix: relay a rate_limit_event that arrives before the turn's first assistant frame (#1534)
## Problem `rate_limit_event` is relayed to the client as `usage_update._meta["_claude/rateLimit"]`, but only when `lastAssistantTotalUsage !== null`. With partial messages on, the SDK emits `rate_limit_event` before the turn's first `assistant` message, while `lastAssistantTotalUsage` is still null (reset at each turn). The SDK emits it once per turn, so in practice clients never receive `_claude/rateLimit`. Observed with a real subscription session (SDK 0.3.316): the SDK sends `stream_event`s, then `rate_limit_event`, then the first `assistant`. The adapter dropped the event and sent only `usage_update`s with `_claude/model`. ## Fix When the event arrives before the turn has any usage, keep its `rate_limit_info` and attach it to the next `usage_update`. That happens in `attachUsageModel`, which every `usage_update` already goes through. A rate limit that the update already carries wins over the pending one. When usage is already known, the event is sent right away, as before. ## Tests - New test in `acp-agent.test.ts`: a `rate_limit_event` before the first assistant frame ends up on the turn's first `usage_update`, and on no later one. It fails without the fix. - `npm run check`, `npm run build` and `npm run test:run` pass. - Checked by hand over stdio with a real session: the turn's first `usage_update` now carries `_claude/rateLimit`, including `unifiedWindows.seven_day`. |
||
|
|
6597626d86 |
fix: list a subagent's background Bash commands (#1540)
Claude Code sends no `tool_use_result` with a subagent's tool results. The adapter recognizes a background Bash command only from the `backgroundTaskId` in that structured result, so it never recognized one that a subagent started. The command's terminal was reported exited (code 0) as soon as the result came, while the command kept running, and the command was missing from the subagent's `state_update.background`. The real SDK stream (CLI 2.1.318) for a subagent running `sleep 30` in the background: ``` task_started task_id=beure6fer task_type=local_bash tool_use_id=toolu_018M… owned_by_subagent=true is_backgrounded=true user parent_tool_use_id=toolu_01HY… tool_result for toolu_018M…, no tool_use_result ``` The local transcripts agree: none of the 35 subagent background Bash results across CLI 2.1.293 to 2.1.318 has a `toolUseResult`. When a Bash result has no structured task id, the adapter now uses the live background `local_bash` task whose `task_started` named that Bash call. The existing subagent test now sends the stream the CLI sends. It fails without the fix. |
||
|
|
ba162346c9 |
fix: list Monitor calls in the session's background work (#1538)
Claude Code marks every live-update watcher `ambient`, "requested or auto-started" per the SDK's `background_tasks_changed` docs, so the task of a `Monitor` call the model made is ambient too. `background-work.ts` left out every ambient task, so a running Monitor was never listed: - its tool call went straight to `completed` instead of staying `in_progress` until its task ends; - a client couldn't stop it with `tool_call/cancel`; - a client deciding whether a session is quiet, for example before restarting or releasing it, saw no background work while the Monitor kept starting turns. Seen live in Delta against 0.101.0: a Monitor watching a loop kept waking the agent every 20 seconds while every `state_update` reported `background: []`. ## Change - `toolResults` registers a task a tool call started even when it is ambient, so the call stays `in_progress` and advertises `cancel`. - The list builder lists a registered call's task before skipping ambient tasks. Ambient tasks without a call of their own, such as watchers Claude Code starts by itself, and ambient terminals stay out. ## Tests - The Monitor fixture in `acp-v2.test.ts` now carries `ambient: true`, as the CLI sends it. Without the fix, the four existing Monitor tests fail with it. - The `_claude/task` test is now an `it.each`: a non-ambient task without a call is listed, and an ambient watcher without a call is not. - `npm run check`, `npm run build`, `npm run test:run` pass. Not run: `npm run test:integration` against the real CLI. |
||
|
|
7ea9eec839 |
feat: start sessions in Auto mode by default (#1533)
Claude Code now starts interactive terminal and VS Code sessions in Auto (v2.1.283+). Agent SDK runs with no `permissionMode` still start in Manual ([docs](https://code.claude.com/docs/en/permission-modes)). Until now the adapter followed the SDK default. ACP clients are interactive editors, so this PR makes them start like the VS Code extension. ## How a session now starts - **No mode configured:** with no `permissions.defaultMode`, or an invalid one that settings resolution drops, a new session starts in `auto`. - **Model without Auto support:** that built-in Auto default starts in Manual (`default`) with no warning, as Claude Code does. A user who explicitly sets `"auto"` still gets the existing fallback to `acceptEdits` with a warning. - **`disableAutoMode: "disable"`:** the adapter ignored this setting before. Now, set at the top level or under `permissions`, it removes Auto from the mode list, the plan-exit options and mode switching, and the session starts in `default`. The bundled CLI reads both spellings. - **Unchanged:** an explicit `permissionMode`, a resumed transcript's mode and a configured `defaultMode` behave as before. ## Limitations - **HIPAA:** Claude Code starts organizations with the HIPAA configuration in Manual. The SDK doesn't report that configuration (it is not in `AccountInfo`, the `init` message or the model list), so those sessions start in Auto here. This is noted in a code comment. - **Mid-session setting changes:** like the bypass setting, `disableAutoMode` is read when the session is created. ## Tests - Updated the tests that expected the old default. - Added cases for the built-in default, the Manual fallback on a model without Auto, and both spellings of `disableAutoMode`. - Regenerated the scenario snapshots for ACP v1 and v2. The 4 `origin-main` baseline files change only the mode value, from `"default"` to `"auto"`. |
||
|
|
a79d07324d |
feat: compact idle sessions before the prompt cache expires (#1522)
Claude Code compacts a large idle session shortly before its one-hour prompt cache expires, but only in attended sessions. SDK sessions (entrypoint `sdk-ts`) never arm that timer, so every return after an hour re-writes the whole context to the cache. The adapter now runs its own timer: 54 minutes after the last main-thread request, if that request used the 1h cache, the context is at least 200k tokens and the session is idle, it sends `/compact` through the normal prompt path. It respects `idleCompaction: false`, `autoCompactEnabled: false` and `DISABLE_COMPACT` / `DISABLE_AUTO_COMPACT`. AIR gets `trigger: "idle"` in the compaction `_meta`. Closes #1454 Closes #1518 |
||
|
|
1d4418f80d |
fix: update to @anthropic-ai/claude-agent-sdk 0.3.318 (#1525)
Updates the Claude Agent SDK from 0.3.316 to 0.3.318, which bundles Claude Code 2.1.318. Also updates `@anthropic-ai/sdk` (1.3.0 → 1.4.0), `vitest` (5.0.5 → 5.1.0) and the transitive dependencies in the lockfile. ## Changes from the changelogs - **`session_writes_lost` (SDK 0.3.317):** a new system message that the CLI sends as it exits, after the last result, when a full disk or used-up quota kept it from saving the transcript or `~/.claude.json`. No turn is open to carry a `session/update` then, so the adapter logs it, as it does `mirror_error`. Without a case for it, the build fails on the exhaustive switch. - **TaskCreate `task.description` (SDK 0.3.317):** the plan now takes a task's subject and description from the TaskCreate result, which holds the task as saved. It differs from the input when Claude Code corrects a malformed call or a PreToolUse hook changes it. This applies to the live stream and to `session/load` replay. ## Reviewed, no change needed - New `startup_failure_reason` values (`config_file_invalid`, `initialize_not_first`) already reach the client as an internal error with the CLI's own message. - `onFailure: "block"` on hook matchers: none of the adapter's hooks guard permissions, and a failed Edit/Write preview must not block the edit. - Removed `file_unchanged` Read output, optional `status` in `PushNotificationInput`, and Grep/Glob `timedOut`: nothing in the adapter reads these. - `workingDirectoriesAsGiven`: it makes Claude Code not follow links at the listed working directories. Listing the ACP `cwd` would put every file of a symlinked workspace outside the working directories. Hosts that want it can already pass it through `_meta.claudeCode.options.settings`. - The comments on the `getSettings` and `unanswered` type workarounds now name 0.3.318; both are still missing from the SDK's types. ## Not updated TypeScript stays on 6.0.3: typescript-eslint 8.72.1 requires `typescript <6.1.0`, so 7.0.2 has to wait for a typescript-eslint release that supports it. ## Testing `npm run build`, `npm run check` and `npm run test:run` pass. New tests cover both changes. `npm run test:integration` was not run. |
||
|
|
e3f0b6c89c |
feat: ship single-file executables built with bun (#1524)
Brings back the standalone executables that #525 and #572 turned off. npm publishing is unchanged. Each stable release also attaches 8 archives (Linux glibc and musl, macOS, Windows, on x64 and arm64) to its GitHub release. ## Why it works now The old executables ran the SDK's JS CLI from inside Bun's `$bunfs`, and it couldn't reach its `vendor/` files there. The SDK's CLI is now a native binary shipped in a per-platform npm package. So each archive ships that binary as `claude` next to `claude-agent-acp`, and the agent runs it from its own directory. Nothing is embedded or extracted, and the agent has no `node_modules` to resolve. The first commit embedded the CLI and extracted it with the SDK's `@anthropic-ai/claude-agent-sdk/extract` helper. That works, but the OS can only run a real file, and Bun-embedded files only exist inside the agent process. So it wrote a second ~300 MB copy to `/tmp` on first run, once per version. Shipping the CLI next to the executable avoids that, and the download size is the same (~180 MB `.tar.gz`). Claude Code's helper tools work too. Native Claude Code has no Grep or Glob tools: its Bash shell wraps `grep` and `find` in functions that run the ugrep and bfs built into the binary at `$CLAUDE_CODE_EXECPATH`. That variable points at the `claude` shipped next to the agent. ## Changes - `src/claude-cli.ts` now owns `claudeCliPath()`, moved out of `acp-agent.ts`. `useBundledClaudeCli(dir)` makes it return `<dir>/claude[.exe]`. If that file is missing, it throws an error saying to keep `claude` next to `claude-agent-acp` or set `CLAUDE_CODE_EXECUTABLE`; before, the user got a bare ENOENT. `CLAUDE_CODE_EXECUTABLE` still wins. - `src/binary-entry.ts` is the executable's entry. It registers the directory of `realpath(process.execPath)`, so a symlink in `/usr/local/bin` works, then imports the agent. It's left out of the npm tarball. - `scripts/build-binary.mjs` (`npm run build:binary`) runs `bun build --compile` for a target and copies that target's `claude` next to the output. - `scripts/smoke-binary.mjs` (`npm run smoke:binary`) runs: - `--version` and `--cli --version` - a symlinked executable - the missing-`claude` error - `initialize` over v1 and v2 - the `terminal-auth` login command, run with `--version` (checks that no `$bunfs` path leaks into it) With `--full` it also drives a real session: - Bash `grep`/`find`, checking that they ran Claude's embedded ugrep/bfs - Read on text and on a PNG - Edit - a subagent - `session/cancel` during a running command - `session/list` - `session/load` in a new process - `session/fork` and `session/delete` - `src/agent-command.ts` builds the `terminal-auth` login command, which reruns the agent. Under Node that's `node <script> ...flags`. In a compiled executable, `argv[1]` is a path inside the executable (`/$bunfs/root/claude-agent-acp`), so it's dropped. Passing it back, as the old `argv.slice(1)` did, handed it to the Claude CLI as an argument and broke `auth login`. - `.github/workflows/binaries.yml` builds and smoke-tests each target on its own platform's runner; the musl targets are tested in an Alpine container. CI runs it on every PR, using the basic smoke test, since CI has no Claude login. On a stable release, `publish.yml` attaches the archives (same names as before). These jobs don't gate the npm publish, and previews get no executables. On stable releases the registry dispatch waits for `upload-binaries`, so the archives (and the digests the registry reads) exist first. It only waits: a failed upload doesn't stop the dispatch. ## Testing I also ran the whole unit suite on Bun's runtime (`bun --bun vitest run`). 3,573 of 3,575 tests pass. The two failures are test artifacts: Bun's `os.homedir()` ignores a `HOME` changed at runtime, and one steer-and-cancel test asserts before the mock delivers its last idle. An idle-accounting trace is identical on both runtimes. On macOS arm64 with Bun 1.4.4, `smoke:binary --full` passes all checks. `CLAUDE_CODE_EXECPATH` pointed at the shipped `claude`, and nothing was written to the temp directory. The basic smoke test also passes from an unpacked archive outside the repo with `PATH=/usr/bin:/bin`, and `node dist/index.js` passes the full run. On Linux, Windows and Intel macOS, only the basic smoke test runs, in this PR's CI. |
||
|
|
8bbdea6cc6 |
fix: honor configured fallback for unavailable restored modes (#1520)
A restored transcript mode such as `dontAsk` is recognized by the Claude SDK but absent from the ACP mode catalog. It previously reached initialization and was forced to Manual, skipping a supported `permissions.defaultMode` from settings. Use the same catalog for restored-mode selection and initialization. Supported restored modes retain precedence; an unavailable restored mode falls back to an available configured mode, then Manual. Explicit initial modes, bypass restrictions, model-specific Auto handling, and new-session configuration behavior remain unchanged. Validation: TypeScript build, ESLint, Prettier, and 243 focused tests pass. Added regressions cover helper selection plus v1 and v2 responses; nine checks fail against unchanged production code. The full local suite **FAILED**: 3,549 passed, 20 failed, 30 skipped. Four selected failures also reproduce on unchanged base: three session-index cases (watcher setup, live notification, SDK path-list parity) and one background-work case. The remaining 16 failures were not individually classified. No test timeouts or product behavior were changed to suppress them. Live CLI and IDE permission-enforcement acceptance remain unverified. Co-authored-by: Codex <codex@openai.com> |