mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-11-11 11:51:14 +00:00
a4a8534c23b08cdfd7f656d25f9ab16068fd8ffd
643 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
1e986b526d |
refactor: give each session one owner of its background work (#1517)
Follow-up to #1510, #1511 and #1512. Clients receive exactly what they did before. ## Why The v2 background work grew as about 14 free functions taking `(session, sessionId, …)` and reaching into six `Session` fields. Four of them it kept itself: the task set, terminals, tool calls, and `reportsBackground`. Three belong to other modules: `liveBackgroundTasks`, the subagent map, and `toolUseCache`. The repo guidelines ask for one module that owns the state. ## What changed - **One owner per session.** `SessionBackgroundWork`, stored on `Session.background`, owns the task set, terminals, tool calls, output tails, and whether the client gets the lists. It has one method per event: a Bash result, tool results, a task's end or pause, a new task set, and a closed query. - **The reporter only filters.** The object builds each session's report, so `BackgroundWorkReporter` only drops unchanged lists and forwards the rest. - **No reference back to the session.** A recreated session (sign-out respawn, provider change) gets a new `Session` under the same id, so an old consumer must not reach the new one. Callers pass in the state other modules own. - **No `toolUseCache` dependency.** Whether a task's launching call has returned used to be judged by the call still being in `toolUseCache`, which Claude Code prunes at the tool's result. The object now records returned calls itself, capped at 1024 like the other per-session sets. - **Stricter typing on the output path.** Typing the terminals map properly exposed that the parsed `outputFilePath` was loosely typed, so it is now checked to be a string. - **Test doubles.** The hand-built `Session` doubles in `acp-agent.test.ts` and `session-doubles.ts` now carry the new required field. ## Tests - The existing suite passes unchanged. - One new v2 test covers a task whose `task_started` arrives after its call returned, so it is listed as `_claude/task`. A first version of this refactor recorded "returned" per task and silently missed that case. The test fails against that version, and against listing tasks before their call returns. `npm run check`, `npm run build` and `npm run test:run` pass (3556 passed, 30 skipped). |
||
|
|
53e3eec013 |
feat: stream the output of v2 background commands (#1512)
Follow-up to #1510 and #1511, phase 2b of the v2 [Background Work RFD](https://agentclientprotocol.com/rfds/v2/background-work). Claude Code streams no output of a Bash command that went to the background: it writes the output to the task's output file. So the command's terminal showed only the Bash result's placeholder ("Command running in background with ID …") until the command exited. The v2 spec says the Agent **SHOULD** keep a listed terminal's output current whenever it can read the process output. ## What a v2 client gets - **Snapshot:** the file's first text replaces the placeholder as a `terminal_output` snapshot. - **Chunks:** later text is appended as `terminal_output_chunk`s. - **Task end:** the rest of the file is read before the terminal's `exitStatus`, as the spec asks. - **Query close:** reading stops. ## Design - New `src/background-output.ts` (`BackgroundOutputTail`) follows one file: - It polls every 500 ms, because watching a file is not reliable on every platform and a missing file just means nothing was written yet. - It reads at most 256 KiB per read, so a burst of output doesn't block the event loop. Each read is sent before the next, including when reading to the end at the task's end. - Past 1 MiB of unread output (a command writing faster than the polls read, about 512 KiB/s), it skips to the file's last 1 MiB and sends it as a replacing snapshot, so the terminal shows the latest output without a gap in it. A file that shrinks is read again from its start, also as a snapshot. - Its timer is unref'd, so a long-running command doesn't keep the agent process alive. - A `StringDecoder` holds a UTF-8 character split across reads until its last byte is written. - Reads are serialised. - `background-work.ts` starts the tail once the Bash result's own updates are sent, so the first output replaces the placeholder rather than coming before it. It finishes the tail before the terminal's exit and stops it when the query closes. `acp-agent.ts` only gets wiring. - The output path comes from the Bash result's text, the only place the SDK gives it before the task ends. The existing `asyncTaskOutputFilePath` parser only accepts an absolute path ending in `tasks/<taskId>.output` for this task. Without a path, the terminal keeps the placeholder as before. - v2 only (`reportsBackground`), like the rest of the background work. v1 clients that render terminals through the `_meta` extension could use it too. That is a small follow-up, but it changes what v1 clients receive. ## Tests - Nine unit tests for `BackgroundOutputTail`: - the replacing first read, and appends - a file that appears later, and a file that never appears - a UTF-8 character split across reads - finishing, an empty output, and stopping - a backlog past 1 MiB, and a file that shrinks - One v2 test over a real connection with a real output file. It checks that the placeholder is replaced, later output arrives as chunks, and the rest arrives before the exit. - I checked by mutation that removing the read-before-exit, the replacing snapshot, the UTF-8 decoder, the backlog cap or the shrink restart each fails a test. `npm run check`, `npm run build` and `npm run test:run` pass (3555 passed, 30 skipped). |
||
|
|
4d79b9ffbc |
feat: list v2 background tool calls (#1511)
Follow-up to #1510, phase 2a of the v2 [Background Work RFD](https://agentclientprotocol.com/rfds/v2/background-work). A Monitor, Workflow or backgrounded MCP call returns as soon as its task starts, so its tool call completed at its result while the task ran on. #1510 listed such tasks as `_claude/task` items, which clients don't recognise. This PR lists them as the tool calls that started them, as the RFD's adapter table describes. ## What a v2 client gets | When | The tool call | |---|---| | Tool result, while the task is in Claude Code's live background task set | Reports `in_progress` instead of `completed`, and is listed as a `tool_call` item | | While listed | Advertises `capabilities.cancel`. `tool_call/cancel` stops the task with `stopTask`, or returns `-32602` if the call isn't listed (also when its task ended mid-cancel) | | `task_updated` with `paused` / `running` | `paused` / `in_progress` | | The task ends | `completed`, `failed`, or `cancelled` for a stopped (`killed`) task, with `capabilities: null`, before the list drops it. The same happens when the live set drops the task without an end event (`completed`, since nothing was reported), or the query closes (`cancelled`) | | `session/resume` | The call's current state, rebuilt from its tool use, then its capability, before the snapshot | The task is matched to its call through the structured `taskId` of a Monitor or Workflow result, or through the live task whose `task_started.tool_use_id` names the call (backgrounded MCP calls). `session/cancel` no longer stops background workflows for sessions that report their background work, because listed work must keep running. Clients stop them with `tool_call/cancel` instead. v1 and AIR are unchanged. A task without such a call is still a `_claude/task` item. It is now listed only once its `task_started` has arrived and the call that started it, if any, has returned. Before that, the foreground work is still waiting on that call, and the spec says foreground work is not listed. ## Design - `background-work.ts` holds the logic: registering background tool calls, pause and end updates, the shared `cancelBackgroundWork` for `terminal/cancel` and `tool_call/cancel`, and the resume states. - `acp-agent.ts` only gets wiring. - `paused` travels as a cast v1 status, alongside the existing `V2_CANCELLED`. - `V2Background` advertises and clears the cancel capability. ## Tests - Six new v2 tests, using the real CLI's event order (tool use, task joins the live set, `task_started`, tool result) and the `killed` + `stopped` pair for a stopped task. They cover: - listing, pause and resume, and the final status - `tool_call/cancel` - a query that closes - a listed Workflow left running on `session/cancel` - resume - the `_claude/task` fallback - I checked by mutation that each new guard fails a test when removed. `npm run check`, `npm run build` and `npm run test:run` pass (3545 passed, 30 skipped). ## Still to come - Tailing background Bash `output_file` into terminal output (phase 2b). - `ScheduleWakeup` / `CronCreate`, which need a signal from the SDK. - The SHOULD gap about follow-up work is being relaxed upstream in agentclientprotocol/agent-client-protocol#2745. |
||
|
|
5565259771 |
feat: report v2 background work (#1510)
Implements phase 1 of the ACP v2 [Background Work RFD](https://agentclientprotocol.com/rfds/v2/background-work), now part of schema v2.0.0-alpha.13. v2 sessions report the work Claude Code keeps running independently of foreground work as `background` on every `state_update`. ## What the client gets The lists are built from Claude Code's live background task set (`background_tasks_changed`). Tasks Claude Code marks `ambient` are skipped, since they are housekeeping. The root session and each subagent session (subagents RFD form) has its own list. Each task is listed once, in the list of the session that runs it: | Work | Listed as | |---|---| | A Bash command that went to the background | Its display terminal, from the Bash result until its task ends. While listed, the terminal advertises `capabilities.cancel`, and `terminal/cancel` stops its task with `stopTask`. | | A detached subagent | Its child session, while it works, and while it is idle with background work of its own. The child reports its own list on its `state_update` and on the parent's `subagent_update.state` mirror. | | Any other task (`Monitor`, `Workflow`, `mcp_task`, …) | A `_claude/task` item with `taskId` and `description`. Its tool call ends at its tool result, so it cannot be a `tool_call` item yet. | The ordering rules the spec requires: - **List changes:** the state is re-sent with the new list and no stop reason, so a repeated `idle` is not a new completion. - **Before a terminal is listed:** a `terminal_update` advertising `cancel` goes out first. - **Before a terminal leaves the list:** its `exitStatus` goes out, with `capabilities: null`. This also holds when the level drops the task without an end event, or when the query closes (process death, stream error, sign-out recreation, `session/close`). - **A child and its parent:** the child's own list goes out before the parent's list drops it. - **`session/resume`:** each listed subagent's current `subagent_update` and each listed terminal's current `terminal_update` come before the state snapshot. - **`terminal/cancel`:** it returns `-32602` for a terminal that is not listed, including one whose command ended while the stop was in flight. ## Gating Only v2 sessions with `perTaskStopAffordance` report lists. That is the default for a non-AIR v2 client. Without it, Claude Code's interrupt kills background tasks, which would break the rule that `session/cancel` leaves listed work running. v1 and AIR are unchanged. ## Design - `src/background-work.ts` owns the lists. It builds them from session state and publishes changes. `acp-agent.ts` only gets wiring calls: one sync per message-loop iteration, the level update, terminal exits when a query closes, and the `terminal/cancel` entry point. - `src/v2/background.ts` adds the lists to v2 state updates and to subagent state mirrors. The cancel capability lives in `V2Terminals`, which already owns terminal state. - `NativeSubagentRuntime.currentUpdate` builds a child's live `subagent_update` for resume. ## Not in this PR - **SHOULD gap:** when finished background work starts a follow-up cycle, the spec wants `running` in the same snapshot that drops the item. We send `idle []` and then `running`, because the adapter can't know in advance that Claude Code will start a follow-up. The RFD acknowledges this. - **Phase 2:** - List `Monitor`, `Workflow` and `mcp_task` as `tool_call` items that stay `in_progress` (or `paused`), with `tool_call/cancel`. - Tail background Bash `output_file` into terminal output. - **Phase 3:** `ScheduleWakeup` / `CronCreate`. The SDK gives no signal for when one fires or is removed. ## Tests - New v2 tests cover: - terminal listing, cancel advertisement, exit ordering, ambient tasks, and a level drop without an end event - query death, `terminal/cancel` including the race, and resume for both terminals and subagents - a background subagent, an idle subagent kept listed by its own command, `_claude/task` items, and sessions without `perTaskStopAffordance` - I checked by mutation that the guards for each required ordering fail a test when removed. - The v2 golden files gain `background: []`, and the subagent ones also gain child lists. `npm run check`, `npm run build` and `npm run test:run` pass (3540 passed, 30 skipped). |
||
|
|
720bbb3e4c |
fix: update to @agentclientprotocol/sdk 2.4.0 (#1509)
Updates `@agentclientprotocol/sdk` from 2.2.0 to 2.4.0 (schema v1.29.1 and v2.0.0-alpha.13). ## What changed in the protocol - **v1:** the stable schema and spec pages only changed wording. There are no requirement changes. - **v2 `session/cancel`:** the Client no longer answers pending permission requests with `cancelled`. The Agent MUST stop waiting for the cancelled work's permission requests and SHOULD cancel each one with `$/cancel_request`. - **v2 `session/close`:** the existing MUST to cancel pending elicitations with `$/cancel_request` before responding now covers permission requests too. The adapter already did both: the tool call's signal and the session's abort controller cancel the request, and a hand-written abort race stopped the wait. Until now, no test checked that a v2 client actually receives `$/cancel_request` for a permission request. ## Changes - Permission requests and elicitations, on both v1 and v2, now use SDK 2.4.0's `stopWaitingOnCancel` option instead of `raceWithAbort`. The SDK sends `$/cancel_request`, rejects at once with `-32800`, and quietly drops any late response. Every caller already maps a rejection after an abort to "Tool use aborted" or to `cancel`, so nothing else changes. `raceWithAbort` is still used for the eager `tool_call` notification. - **Tests:** - **v2:** the existing tests where a `session/cancel` or `session/close` withdraws a held request now also check that the client received `$/cancel_request`. For `session/close`, it must arrive before the response. - **v1:** new tests over a real SDK connection with a client that holds requests. These cover the v1 `ClientConnection` wiring, which the mocked-client tests bypass. - The v1 mock clients that "ignore cancellation" now model the SDK's behaviour with a `heldRequest` helper. - Removed one test that only covered `raceWithAbort` handling a rejection from the client. - I checked that the new assertions fail if the `stopWaitingOnCancel` option is removed or v2 permission requests stop passing their cancellation signal. ## Follow-ups (not in this PR) - v2 background work: the `background` list on state updates, the `paused` status, and `tool_call/cancel` / `terminal/cancel`. The AIR `asyncTasks` extension stays as it is. - The new unstable v1 methods (`session/set_title`, `session/archive`/`unarchive`, `session/status`, `auth/status`). `npm run check`, `npm run build` and `npm run test:run` pass (3520 passed, 30 skipped). |
||
|
|
38cf656fc9 |
feat: report why Claude Code could not start a session (#1508)
When Claude Code refuses to start (invalid proxy URL, wrong organization sign-in, a required sandbox that can't run, …), `session/new`, `session/load`, `session/resume` and `session/fork` used to fail with a generic `Claude Code process exited with code 1. stderr: …` error. This sets `CLAUDE_CODE_STARTUP_FAILURE_RESULTS=1` on the query env, so the CLI writes an error result with a `startup_failure_reason` before exiting on a known startup failure. The SDK then rejects `initializationResult()` with an error that carries `startupFailureReason`. The new `src/startup-failure.ts` maps it to an ACP error: | `startupFailureReason` | ACP error | |---|---| | `gateway_signin_required`, `org_pin_mismatch`, `org_config_refused` | `authRequired` (signing in again, or with another account, fixes it) | | `cwd_unavailable` | `invalidParams` (matches the existing cwd validation) | | any other reason | `internalError` | The message is the CLI's own text, which says what to fix (e.g. `Invalid proxy URL in HTTPS_PROXY: … Fix or unset HTTPS_PROXY and restart Claude Code.`), and `data.startupFailureReason` carries the reason. Errors without a reason (older CLIs, unknown causes) pass through unchanged, and the existing `sessionNotFound` mapping for resumes is untouched. Checked against the real CLI (SDK 0.3.316): with `HTTPS_PROXY="not a url"`, `initializationResult()` rejects with `startupFailureReason: "proxy_invalid"` when the variable is set, and with a plain exit-code error without it. ## Testing - v1: `create-session-options.test.ts` covers the env var, each mapping, and pass-through of errors without a reason. - v2: `acp-v2.test.ts` covers the `session/new` error over the wire. - `npm run build`, `npm run check`, `npm run test:run` pass. The subprocess integration tests also passed locally with this change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
1d0306468b |
fix: update to Claude Agent SDK 0.3.316 (Claude Code 2.1.316) (#1507)
Updates dependencies to their latest versions. - `@anthropic-ai/claude-agent-sdk` 0.3.312 → 0.3.316 (bundles Claude Code 2.1.316) - `@types/node` 26.6.5 → 26.6.6, `eslint` 10.13.0 → 10.14.0, `globals` 17.13.0 → 17.14.0 - `npm update` for transitive patch/minor bumps in the lockfile - Bumps the "as of" version in two comments about SDK type gaps (`Query.getSettings()` still missing from the declarations; `unanswered` still only in the AskUserQuestion output schema). Both were re-checked against 0.3.316. TypeScript 7.0.2 is left out: `@typescript-eslint` 8.72.1 supports TypeScript `<6.1.0`. ## Changelog review (SDK 0.3.313–0.3.316, Claude Code 2.1.313–2.1.316) No required changes. The SDK type changes only add fields and union members (`sandbox_required_unavailable` startup failure reason, `blockWritesOutsideWorkingDirectories` / `blockReadsExemptPaths` settings, `pending_elicitation_requests` on `initialize`, alpha `prewarm()` in `/core`), none of which the adapter reads or needs. No new `fast_mode_disabled_reason` values accompany the Opus 4.8 fast mode deprecation. Follow-up planned separately: use `CLAUDE_CODE_STARTUP_FAILURE_RESULTS` / `startup_failure_reason` to report typed startup failures to clients. ## Testing `npm run build`, `npm run check`, `npm run test:run` pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
eb78afa855 |
feat: report subagent working directories (#1496)
Schema v1.28.0 / v2.0.0-alpha.11 adds `cwd` and `additionalDirectories` to `subagent_update` (agentclientprotocol/agent-client-protocol#2593, #2660). When a child runs in a different directory from its parent, for example an Agent call with `isolation: "worktree"`, the Agent must report the child's `cwd` before sending child traffic that depends on it. **Signal** - The agent reads the `cwd` of the child's own hooks, matched by `agent_id`. The existing per-child configuration hook already runs on SubagentStart, PreToolUse, PostToolUse and the others. - Claude Code runs SubagentStart before the child's first model request and waits for the hook, so the `cwd` reaches the client before the child's first tool call. - `worktreePath` in the Agent tool result arrives only when the child finishes, which is too late. **Behaviour (RFD form only; AIR is unchanged)** - `cwd` is sent only when the child's directory differs from its parent's, compared with `realpath`. A hook that runs before the announcement puts the `cwd` in the announcing `subagent_update`. Otherwise a separate `subagent_update { cwd }` follows. - A child that moves back to its parent's directory gets `cwd: null`. A child whose hook `cwd` changes later, for example after a `cd` that Claude Code keeps, is reported again, since that is where it now runs. - `additionalDirectories` is never sent. Claude Code gives subagents the session's additional directories, so inheriting them is accurate. - Replay uses the child's `.meta.json` (`cwd`, `worktreePath`, `inheritedWorktreePath`) and the first `cwd` in its transcript. After the load response, a child whose directory no longer exists gets `cwd: null`. - v2 passes the fields through unchanged. **Not covered** - `remote` isolation, which fires no local hooks. - After a reconnect to a live session, the `cwd` the runtime holds is not sent again. |
||
|
|
d2ef490a31 |
fix: validate session, prompt, config, and auth requests per the latest ACP spec (#1495)
Request validation from the ACP schema v1.28.0 / v2.0.0-alpha.11 review. Each change has a test. **Paths** - `session/list` rejects a relative `cwd` with -32602. Without a session index it passes `includeWorktrees: false`, so the filter matches only sessions of exactly that directory, not other git worktrees of the same repo. - Resume, load and fork check `cwd` and `additionalDirectories` before anything else. Before, a resume with a bad `cwd` tore down the live session and then failed, and a fork copied the transcript first. - Relative `additionalDirectories` entries are rejected with -32602. **Session setup (v2)** - Uses the SDK's `assertSupportedMcpServers`, which also rejects duplicate MCP server names with -32602. - Rejects an empty `prompt` with -32602. - The v2 initialize response no longer advertises the AIR `asyncTasks` capability or the `goal` entry, since the v2 app has no handlers for `_session/async_task/stop` or `_session/goal`. v1 AIR is unchanged. **Errors** - `session/fork` of an unknown session returns -32002 with `data.sessionId` instead of -32603. This matches the Claude SDK's "Session … not found" and "Invalid sessionId" messages. - `authenticate` with an unknown `methodId` returns -32602 instead of -32603. - `set_config_option` rejects a value whose type doesn't match the advertised option type. A `type: "boolean"` option takes `true`/`false`, and a select option takes a string. Model aliases such as `"opus"` are still accepted. - An AIR `session/delete` of a session that has no transcript now succeeds instead of failing with a generic error. **Prompts and commands** - v1 `session/prompt` rejects `audio` and unknown content blocks with -32602 before queueing them. Before, they were silently dropped. - Advertised command names that contain whitespace are dropped, and of duplicate names only the first is kept. - The `/mcp:server:prompt` rewrite now accepts arguments that span lines, and applies only to the first text block. |
||
|
|
f50598a39c |
fix: align session/cancel, session/close, and v2 setup with the ACP v2 spec (#1494)
**`session/cancel` cancels only earlier prompts** (agentclientprotocol/agent-client-protocol#2685) - `cancelTurns` used to read the turn queue again after two awaits: the compaction interrupt and publishing the subagent state. A prompt that arrived during those awaits was cancelled too, which is -32800 on v2. - Each prompt now gets a sequence number when `startTurn` receives it. The cancel records the count before its first await and cancels only turns at or below it. That includes a prompt received earlier that `startTurn` had not queued yet. - Known gap: if the cancelled turn ends during those awaits and Claude Code starts the newer prompt, the `query.interrupt()` that follows can still stop that prompt. Fixing it means skipping the interrupt when nothing the cancel covers is still running, which also affects cancelling background work, so it is left for later. **`session/close` cancels pending requests first** (agentclientprotocol/agent-client-protocol#2683) - MCP elicitations, AskUserQuestion, the refusal-fallback dialog and permission requests now combine their callback signal with the session's `abortController.signal`. - `teardownSession` therefore sends `$/cancel_request` for each of them before `session/close` responds, instead of relying on Claude Code to abort its callback signals in time. **v2 setup responses** (agentclientprotocol/agent-client-protocol#2688, #2603) - If reading the commands for the setup response fails, the agent now sends them in an `available_commands_update` after the response. Before, the client was never sent them. - On v2, MCP OAuth starts after the `session/new`, resume or fork response is sent, so its URL elicitation no longer reaches the client before the response. Not done: withdrawing a held prompt with `$/cancel_request`. The user message is already in Claude Code's input once it is queued, and removing it safely needs `cancel_async_message`, which not every CLI version has. agentclientprotocol/agent-client-protocol#2636 allows ignoring the cancel and sending the normal response. |
||
|
|
3bc671193c |
fix: forward v2 elicitation values and read dismissed v2 permission prompts as a refusal (#1493)
**Elicitation content** (agentclientprotocol/agent-client-protocol#2595)
- v2 lets an elicitation answer hold any JSON value, such as an object.
The agent checked accepted content with the v1 guard, which allows only
strings, numbers, booleans and string arrays. A v2 accept with an object
therefore reached the MCP server as `{ action: "accept", content: {} }`.
- The agent now forwards the accepted content unchanged, and the MCP
server checks it against its own schema. If Claude Code's MCP client
still uses the older value types, it rejects such an answer with an
error instead of passing on empty content.
- AskUserQuestion reads only string, number and boolean picks, so it no
longer shows an object as `[object Object]`.
- The MCP OAuth url-mode check now tests `action !== "accept"` instead
of the v1 guard.
**Permission `cancelled` on v2**
(agentclientprotocol/agent-client-protocol#2582, #2658)
- In v2, `cancelled` also means the user dismissed the prompt or the
client declined it by policy.
- When no `session/cancel` is in progress, a foreground v2 `cancelled`
is now denied as "User refused permission to run tool", the same denial
background subagents already get, and the turn goes on. Before, Claude
Code reported it as "Tool permission request failed".
- During a `session/cancel` it is still aborted. v1 is unchanged.
- Unknown v2 outcomes map to `cancelled`, so they are also denied now
instead of aborted.
|
||
|
|
c3428de1bf |
fix: keep native Windows paths in v2 git patches (#1492)
ACP v2 now says each `git_patch` path, once unquoted, is the same native absolute path string as the matching `changes` entry (agentclientprotocol/agent-client-protocol#2650). The v2 form converted `C:\work\App.ts` to `C:/work/App.ts`, so on Windows the patch and `changes[].path` named different strings. The v2 form now keeps the native path, which Git quoting writes as `"C:\\work\\App.ts"`. Git's own form, which the AIR extension uses, still uses forward slashes and the `a/`/`b/` prefixes. |
||
|
|
1d2b92feae |
fix: update to @agentclientprotocol/sdk 2.2.0 (#1491)
Moves to ACP schema v1.28.0 and v2.0.0-alpha.11.
- v2 dropped JSON-RPC batching, so `AnyWireMessage` is gone. The test
harnesses use `AnyMessage`, and a request id can now be `null` in the
types.
- The v2 elicitation schema now requires `properties`. A forwarded MCP
schema without them, such as `{ "type": "object" }`, gets an empty map,
so clients on SDK 2.2.0 don't reject it.
Runtime changes that come with the SDK and need no agent code:
- The SDK handles a repeated `initialize`, rejects batches, and rejects
messages sent before initialization. A failed `initialize` now leaves
the connection open.
- v2 `usage_update.cost` is now a patch field, where omitted means
unchanged. The agent sends `cost` only on the result update, so the
client keeps showing the last cost during a turn.
Follow-up PRs for the spec changes in this range are stacked on this
branch.
|