Compare commits

...

17 Commits

Author SHA1 Message Date
Mark Tkachenko
c331e178a5 fix: explain why Claude Code turns Fast mode off (#1602)
When the user turns Fast mode on, Claude Code can report
`fast_mode_state: "off"` on the next turn's `system`/init. The adapter
mirrored that into the option but explained it only for reasons listed
in `FAST_MODE_UNAVAILABLE_EXPLANATIONS`. `preference`, `unknown` and any
newer code were dropped, so the toggle snapped off with no reason. This
happens on every turn behind an API gateway that authenticates with
`ANTHROPIC_AUTH_TOKEN` only: Claude Code treats Fast mode as disabled by
the organization (`preference`) without asking the server.

Changes:
- `preference` ("not enabled for your organization or API gateway") and
`unknown` ("currently unavailable") are explained. A reason code the SDK
adds later gets a generic `not available (<code>)`.
- Only `sdk_opt_in_required` (every SDK session starts there; the toggle
is the opt-in) and `pending` (eligibility still resolving) stay
unexplained.
- The notice rule is unchanged: Fast mode was on, then off with an
explained reason. Off without a reason stays quiet, because a `result`
can carry the state from the start of the turn.
- One shared function in `session-fast-mode.ts` serves v1 and v2. In
`src/v1/acp-agent.ts` only comments change.

Limitation: the adapter still offers the toggle up front. Until a
session opts in, Claude Code reports `sdk_opt_in_required` and hides the
account or organization blocker, so the adapter can't tell in advance
that Fast mode will be refused.

Tests: unit tests for each reason, plus v1 and v2 flow tests (set `fast`
on, prompt, init reports off/`preference`). The flow tests fail without
the fix.
2026-11-11 17:28:40 +01:00
Mark Tkachenko
830f05a0ac refactor: move the v1 session fingerprint out of acp-agent.ts (#1601)
Moves `computeSessionFingerprint` and its helpers (`normalizeSkills`,
`OPTION_REBUILDS_SESSION`, `canonicalJson`) verbatim from
`src/v1/acp-agent.ts` into `src/v1/session-fingerprint.ts`. No behaviour
change.

- Only `computeSessionFingerprint` is exported, as before; the helpers
stay private to the new module.
- `session-fingerprint.ts` imports nothing from `acp-agent.ts`. The test
that used `computeSessionFingerprint` now imports it from the new
module.
- Emitted JS differs from `main` only in `dist/v1/acp-agent.js` (one
import, the moved code removed) plus the new module.

Review with `git diff --color-moved` to see the move.
2026-11-11 15:31:40 +01:00
Ben Brandt
4df69996ca fix: report a mid-turn API error notice where the response broke off (#1599)
## Problem

When a response stream drops after some blocks arrived, Claude Code
keeps the partial response and writes its own message into the
conversation, e.g. "API Error: Connection lost mid-response. The
response above may be incomplete."

If the partial response contained tool calls, Claude Code runs them and
the turn goes on. Since #1582 the v2 agent sends this text as a
`notice`, but only at the turn's result. In a turn that goes on, the
notice arrived after all the later work, so "the response above" pointed
at the wrong output.

## Change

The v2 agent sends the pending notice as soon as the main conversation
goes on past the failed request: at the next tool result, stream event
or assistant message.

A turn that ends at the failed request behaves as before: the idle's
`error` carries it when the turn fails, and a notice goes out at the
turn's end otherwise.

## Not changed

- v1 keeps its current behaviour, as in #1582.
- `session/load` replay still shows these messages as text.

## Testing

- New v2 case: a kept tool call, the API error message, the tool result,
then more output. The notice comes before the later text. It fails
without the change.
- The test double follows the message shape and order read from the
bundled CLI's code. Not run against a real dropped connection.
- `npm run check`, `npm run build` and `npm run test:run` pass.
2026-11-11 13:45:06 +00:00
Ben Brandt
ecd066c870 fix: say why the login failed in the v2 authRequired error (#1598)
## Problem

When an API request fails authentication, the v2 agent hides the message
Claude Code writes for it (#1582) and fails the turn with a bare
`Authentication required`. The reason is lost, for example:

> Failed to authenticate: this login reached its time limit and cannot
be renewed. Run `claude auth login` to sign in again, or use `claude
setup-token` and set CLAUDE_CODE_OAUTH_TOKEN for unattended jobs

A client can then only say that the agent is signed out, not why.

## Change

The turn's `authRequired` error now carries the result's error text in
its message: `Authentication required: <text>`. The code and `data` are
unchanged, so a client that starts its own sign-in from the code behaves
as before.

Text that tells the user to run `/login` is still left out, as that
instruction is for Claude Code's terminal (#863). Only the v2 agent
changes.

## Testing

`npm run check`, `npm run build` and `npm run test:run` pass. The v2
tests now pin both messages: the plain one for the `/login` text, and
the one with the reason for an authentication failure reported on the
assistant message.

Not checked against a real expired login.
2026-11-11 13:34:58 +00:00
Mark Tkachenko
a22a797db2 refactor: move the v1 steering and async task stop requests out of acp-agent.ts (#1581)
Moves the `_session/steering` and `_session/async_task/stop` request
definitions verbatim from `src/v1/acp-agent.ts`, like
`goal-extension.ts` does for goals. No behaviour change.

- `src/v1/steering.ts`: `STEER_METHOD`, the steer priorities,
`SteerRequest`, `SteerResponse`, `parseSteerRequest`.
- `src/v1/async-task-stop.ts`: `ASYNC_TASK_STOP_METHOD`, its request and
response types, `parseAsyncTaskStopRequest`.
- Neither module imports `acp-agent.ts`. The one test that imported
`SteerRequest` from `acp-agent.js` now imports it from `steering.js`.
- Emitted JS differs from `main` only in `dist/v1/acp-agent.js` (imports
and the moved code) plus the two new modules.

Review with `git diff --color-moved` to see the move.
2026-11-11 14:20:55 +01:00
nikita-ashihmin
4f690e129c feat: say the cause of an api retry in the AIR warning details (#1594)
## Summary

The AIR warning for an SDK `api_retry` message had only a title, such as
`Retrying Claude, attempt 2 of 10.`. The user did not see why Claude
retries or how long the next attempt waits. The warning now carries
`details` with the cause and the wait.

## Changes

- `apiRetryWarning(message)` in `src/session-failure-extension.ts`
builds the title and the details. The title text does not change. The
`api_retry` case of `src/v1/acp-agent.ts` only calls it.
- The cause:
  - `HTTP 529 (overloaded).` when the CLI sends a status;
- `Connection error: no HTTP response.` for a null status with the
`unknown` kind;
- `No HTTP status (overloaded).` for a null status with a kind. The CLI
sends `overloaded` with no status for an SSE `overloaded_error` event
after HTTP 200, so "no HTTP response" would be wrong there.
- The wait: `Next attempt now.` for 0 ms (a `no_response` retry), `Next
attempt in 8s.`, and whole minutes from 120 s (a background rate-limit
wait), e.g. `Next attempt in 30m.`.
- The `no_response` wait stays in the title only.
- The kind label map has the type
`Record<Exclude<SDKAssistantMessageError, "unknown">, string>`, so a new
SDK kind fails typecheck. A missing `error` reads as `unknown`.
- The 401 skip, the actions and the terminal failures do not change. The
subagent retry path sends a `tool_progress` update and no session
failure, so it does not change. v2 has no `api_retry` session failure.

## Tests

- `src/tests/session-failure-extension.test.ts` (new): 0 ms, minutes,
the 119 s / 2 m boundary, a null status with `cloud_credential_error`,
and a missing `error`.
- `src/tests/v1/acp-agent.test.ts`: the warning details on the wire,
revision 2 replaces the details, and the terminal revision has none. The
test doubles use status and kind pairs and delays that the bundled CLI
really sends.
- `npm run build`, `npm run check`: clean.
- `npx vitest run src/tests/session-failure-extension.test.ts
src/tests/v1/acp-agent.test.ts`: 766 passed, 9 skipped.
- `npm run test:run`: 17 tests in `create-session-options.test.ts`,
`providers.test.ts` and `acp-v2.test.ts` fail on `main` without this
change too. They read the model list of the local environment.
2026-11-11 14:19:41 +04:00
acp-release-bot[bot]
51f514d73c chore(main): release 0.106.0 (#1588)
🤖 I have created a release *beep* *boop*
---


##
[0.106.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.105.0...v0.106.0)
(2026-11-10)


### Features

* report the auth detail as the account's description
([#1591](https://github.com/agentclientprotocol/claude-agent-acp/issues/1591))
([f6d6429](f6d6429187))
* serve session/options and initial config options
([#1592](https://github.com/agentclientprotocol/claude-agent-acp/issues/1592))
([b1763c7](b1763c750a))


### Bug Fixes

* honour the client's settings option in idle compaction
([#1555](https://github.com/agentclientprotocol/claude-agent-acp/issues/1555))
([b73ee49](b73ee491c8))
* replay the messages before each compaction on ACP v1 and v2
([#1430](https://github.com/agentclientprotocol/claude-agent-acp/issues/1430))
([626015b](626015bf81))
* stop the release executable from loading the project's .env and
bunfig.toml
([#1589](https://github.com/agentclientprotocol/claude-agent-acp/issues/1589))
([4a3ff6a](4a3ff6a1eb))
* update to @agentclientprotocol/sdk 2.6.0
([#1590](https://github.com/agentclientprotocol/claude-agent-acp/issues/1590))
([9fca22a](9fca22a03b))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: acp-release-bot[bot] <246668977+acp-release-bot[bot]@users.noreply.github.com>
2026-11-10 22:45:08 +00:00
Ben Brandt
b1763c750a feat: serve session/options and initial config options (#1592)
Implements the unstable [Session options
RFD](https://agentclientprotocol.com/rfds/session-options) on ACP v1 and
v2. Both cores advertise the `options` session capability.

## `session/options`

Answers with what a session would start with: modes and config options
on v1, config options and commands on v2.

- A session that the connection holds answers with what it has now.
- Otherwise a query starts as the session's would, without MCP servers,
and closes once its options are read. With `sessionId` it resumes that
session, and an unknown one fails with `-32002`.
- It sends no update and registers no session.

Claude Code only reports its models, modes and agents from a running
process, so there is no way to answer without starting one. The query
runs with `persistSession: false`, so it stores no session and does not
write to a resumed transcript. Two consequences: each call costs a CLI
start, and the user's `SessionStart` hooks run in it. The v2 commands
lack MCP prompts, which the RFD allows.

On v1 this is an `optionsOnly` exit in `createSession`, after the config
options are built and before the session is registered. On v2,
`launchSession` is split into `openQuery` (start the query, build the
config) and the part that makes the session active, and
`session/options` uses only the first.

## Initial `configOptions`

`session/new`, `session/load` (v1) and `session/resume` take
`configOptions`. They are set in list order through the same code as
`session/set_config_option`, so a model entry decides which effort
levels a later entry can pick.

- Two entries with the same `configId` fail with `-32602` before a query
starts. So does a value type that v2 does not take.
- An entry with an unknown `configId`, a value of another type, or a
value the option does not offer is skipped. An entry that Claude Code
refuses is skipped and logged.
- No update goes out for the entries. The response carries the modes and
options, and the warning of an Auto mode fallback waits for the next
prompt, as it does at a session's start.

`session/fork` does not take them yet: the schema has no field for it.
agentclientprotocol/agent-client-protocol#2794 adds one.

## Testing

New tests in `src/tests/v1/create-session-options.test.ts` and
`src/tests/v2/v2.test.ts` cover both versions. The scenario golden files
change only by the new capability in `initialize`.

Not verified against a real Claude Code yet: `persistSession: false`
together with `resume`. The tests run against the mocked SDK.
2026-11-10 22:42:28 +00:00
Ben Brandt
f6d6429187 feat: report the auth detail as the account's description (#1591)
Stacked on the SDK 2.6.0 update (`acp-sdk-2.6.0`). Merge that first.

`auth/status` and `auth/status_update` carried the detail of an account,
such as the key source or the gateway host, in parentheses in its label:
`Anthropic API key (ANTHROPIC_API_KEY)`. SDK 2.6.0 adds
`AuthAccount.description` for it, so the account is now `label:
"Anthropic API key"` with `description: "ANTHROPIC_API_KEY"`.

`_meta.claudeCode.detail` still carries the detail for clients that read
it there. A client that shows only `label` no longer shows the detail.

The mapping is shared, so v1 and v2 both change.
`docs/air-extensions.md` is updated.
2026-11-10 22:34:49 +00:00
Ben Brandt
9fca22a03b fix: update to @agentclientprotocol/sdk 2.6.0 (#1590)
Updates `@agentclientprotocol/sdk` from 2.5.0 to 2.6.0 (schema v1.31.0
and v2.0.0-alpha.15).

The new schema adds `configOptions` to session setup requests. The v2
core converts a setup request to its v1 form, and the v2 entries are not
assignable to the v1 type, so `v1NewSessionRequest` leaves them out.
Nothing reads them yet: a Client only sends them to an Agent that
advertises `session.options`. #1592 serves them.

No behavior change. `npm run check`, `npm run build` and `npm run
test:run` pass.
2026-11-10 22:34:49 +00:00
Ben Brandt
4a3ff6a1eb fix: stop the release executable from loading the project's .env and bunfig.toml (#1589)
Fixes #1579.

## Problem

The release executable is built with `bun build --compile`. By default a
compiled Bun executable loads `.env`, `.env.local` and `bunfig.toml`
from its working directory at startup. The editor starts the agent in
the user's project, so:

- The project's dotenv values land in the agent's `process.env` and are
passed on to Claude and every Bash command as real environment
variables. Most frameworks never override a variable that is already
set, so the project's own `.env.test` is silently ignored. In the
reported case this made a test suite drop the dev database.
- A `bunfig.toml` in the project that Bun cannot parse stops the agent
from starting (`SyntaxError: An error occurred while parsing code`, exit
1).

Only the executable is affected; running the npm package under Node does
not load these files.

## Fix

`scripts/build-binary.mjs` passes `--no-compile-autoload-dotenv` and
`--no-compile-autoload-bunfig`. Bun's other two autoloads
(`tsconfig.json`, `package.json`) are already off by default.

## Testing

`scripts/smoke-binary.mjs` gains a check that runs the executable in a
directory containing a `.env`, a `.env.local` and an unparseable
`bunfig.toml`, with Node standing in for `claude` to print the
environment the CLI is spawned with. It fails on an executable built
without the flags and passes with them.

Run locally on macOS arm64 only; the Linux, Alpine and Windows targets
are covered by the `binaries.yml` matrix.
2026-11-10 21:27:42 +00:00
Erdős Péter
b73ee491c8 fix: honour the client's settings option in idle compaction (#1555)
## Problem

A client can pass Claude Code settings in
`_meta.claudeCode.options.settings`, and the CLI gets them as
`--settings`. Idle compaction ignored them: it read only
`settingsManager.getSettings()`, the settings files. A client that sent
`{ idleCompaction: false }` still got `/compact` after about 54 idle
minutes, though the CLI had the setting off. The same went for
`autoCompactEnabled` and the `env` block (`DISABLE_COMPACT`,
`DISABLE_AUTO_COMPACT`, `CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS`). This
affected both ACP v1 and v2.

## Change

- `SettingsManager.getCompactionSettings(override)` resolves
`idleCompaction`, `autoCompactEnabled` and `env` in the CLI's order:
managed settings, then `--settings`, then the user/project/local files.
`env` merges entry by entry. An `undefined` value in the override does
not hide the files' value. This sits next to `getApiKeyHelper` and
`getModelSelectionSettings`, which already take the override the same
way.
- v1 (`src/v1/acp-agent.ts`) and v2 (`src/v2/reports.ts`, via
`ReportsSetup.configuredSettings`) pass the client's settings to it.
`IdleCompaction` itself is unchanged.

## Tests

- `settings-api-key-helper.test.ts`: managed > override > files for each
key and each `env` entry; an `undefined` override key keeps the files'
value.
- `v1/idle-compaction-agent.test.ts` and `v2/acp-v2.test.ts`: a session
started with `settings: { idleCompaction: false }` gets no `/compact`.
Both fail without the wiring change.

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Erdős Péter <6430206+sodre90@users.noreply.github.com>
2026-11-10 21:24:24 +00:00
Hussain Hammad
626015bf81 fix: replay the messages before each compaction on ACP v1 and v2 (#1430)
Fixes #1297.

After `/compact`, `session/load` replays from the summary of the last
compaction only. `getSessionMessages` follows `parentUuid` links, and
Claude Code writes each `compact_boundary` with `parentUuid: null`, so
the chain ends there. The messages before it are still in the `.jsonl`,
and the boundary still links the last one of them in
`logicalParentUuid`; nothing follows that link. The spec requires the
agent to "replay the entire conversation", and the replay test `replays
a compaction as one completed compaction_update at its position`
(`v2/acp-v2.test.ts`) already mocks a prompt before the summary, which
the real SDK never returns.

This now happens without `/compact` too: since #1522, idle sessions
compact on their own. It reproduces in Zed as well
([zed-industries/zed#66523](https://github.com/zed-industries/zed/issues/66523#issuecomment-6644375981)).

- When the chain starts at a compact summary, replay reads the
transcript again through `readHistoryAcrossCompactions`
(`resumed-session.ts`). It loads every record with
`loadFullSessionHistory`, which moves from `fork-session.ts` (where it
reads the inactive branches for the same reason) to sit beside
`findTranscript`. It links each boundary to its `logicalParentUuid` and
passes the records back to `getSessionMessages` as a `sessionStore`. The
SDK's own parsing (queued commands, meta messages, deliveries) is
unchanged.
- The boundary's `preservedSegment` and `preservedMessages` are dropped.
With them, `getSessionMessages` moves the messages a compaction kept to
after the summary, and the relinked boundary then leads from them back
to the summary, so the chain loops and ends there. Without them, the
kept messages replay once, where they were said.
- The v1 agent's `session/load` and the v2 agent's replay both read the
history this way.
- A session without a compaction reads its transcript once, as before.
Resume, fork and rewind still use the active chain. If the second read
fails, replay logs it and sends the active chain, as it does today.

## Testing

- `npm run check` and `npm run build` pass. `npm run test:run` passes
except for 2 tests that fail the same way on `main` on this machine:
`v1/acp-scenarios.test.ts > normalize > replaces only the version of
this adapter and the path of this Node binary` (Node at
`/usr/bin/node`), and `v2/v2.test.ts > lists bypass before auto in
ExitPlanMode's options after plan mode left bypass` (this machine runs
as root, where bypass is not offered).
- New: `compacted session replay` (`v1/acp-agent.test.ts`) replays a
real transcript file with one `/compact` between two prompts to a client
that declares `session.compaction`. It fails on `main` without this
change: the replay has no "hi" and no "Hello".
- New: `replays the messages before a compaction from the transcript`
(`v2/v2.test.ts`) resumes the same transcript on v2 with `replayFrom:
start`. Without the change, the replay has no user message before the
compaction.
- New: `readHistoryAcrossCompactions` (`resumed-session.test.ts`) covers
two compactions, a compaction with both forms of preserved metadata as
Claude Code 2.1.293 writes them, and a session without a compaction.
- On 10 local transcripts with 1 to 10 compactions, no message replays
twice. 9 of them now replay from the first prompt with one summary per
compaction, for example 414 → 1925 messages and 909 → 3693. The tenth
has many records written a second time, and replays 5 of its 10
compactions (1428 → 7526 messages). The second read took 4–220 ms, and
745 ms for that 33,000-line transcript.
- Rechecked with 4 transcripts written by Claude Code 2.1.280 to
2.1.318, with 1, 2, 3 and 6 compactions. `session/load` sends one
`compaction_update` per compaction, where `main` sends only the last
one; for the 6-compaction transcript `main` replays nothing before the
last summary. No summary replays as a user message and no tool call
replays twice.

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2026-11-10 22:10:24 +01:00
acp-release-bot[bot]
1ffbb6b577 chore(main): release 0.105.0 (#1583)
🤖 I have created a release *beep* *boop*
---


##
[0.105.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.104.1...v0.105.0)
(2026-11-10)


### Features

* serve session/set_title on ACP v1 and v2
([#1585](https://github.com/agentclientprotocol/claude-agent-acp/issues/1585))
([a4a8534](a4a8534c23))
* serve session/status on v1 and v2
([#1584](https://github.com/agentclientprotocol/claude-agent-acp/issues/1584))
([6bc2e05](6bc2e058cb))


### Bug Fixes

* include inline model settings in the model picker
([#1488](https://github.com/agentclientprotocol/claude-agent-acp/issues/1488))
([36cb674](36cb674589))
* keep a v2 session idle when the model changes between turns
([#1586](https://github.com/agentclientprotocol/claude-agent-acp/issues/1586))
([f5d7916](f5d79167a6))
* report Claude Code's API error message as a notice in v2
([#1582](https://github.com/agentclientprotocol/claude-agent-acp/issues/1582))
([99cc1f3](99cc1f3245))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: acp-release-bot[bot] <246668977+acp-release-bot[bot]@users.noreply.github.com>
2026-11-10 21:51:06 +01:00
Slapoguzov Alexander
36cb674589 fix: include inline model settings in the model picker (#1488)
When `CLAUDE_MODEL_CONFIG` or caller-provided SDK settings list a model
that the SDK omits, include those settings when rebuilding the picker.
Configured models such as Bedrock Haiku versions appear with their
provider override IDs and can be selected in both ACP v1 and v2.

Merge file and inline model lists with the SDK's array semantics,
preserve managed allowlists and override precedence, and keep the
existing shallow caller-over-environment merge. The refreshed change
preserves upstream's resumed-model alias handling and uses the v2 setup
path introduced on current main.

This repairs explicitly configured models; it does not discover an older
model from a gateway listing or change the SDK's family defaults.

Validation on upstream main `a1d83c60310f33cbc3cd78f6b6133278e754efd5`
with SDK 0.3.321:
- TypeScript compilation, ESLint and Prettier checks passed using their
installed executables (the host npm script launcher exits 194).
- 181 focused tests passed, including managed policy and environment,
caller-object and caller-file settings; both v1 and v2 select the
configured provider ID.
- The three new v2 regression cases fail on unchanged main because the
configured models are missing.
- Full suite passed: 3,952 tests, 33 skipped. The first sandboxed run
could not open a local socket or receive filesystem watcher events; the
unchanged candidate passed with host test execution.
- No real model inference or live provider calls were used.

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Aleksandr Slapoguzov <aleksandr.slapoguzov@jetbrains.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 21:11:22 +01:00
Ben Brandt
f5d79167a6 fix: keep a v2 session idle when the model changes between turns (#1586)
On ACP v2, changing the model of an idle session left the session
reporting `running` with no work in progress, and it never reported
`idle` again until some later turn ended. A client that refuses prompts
while the session is `running` could then never send the prompt that
would end it.

## Cause

After `setModel`, Claude Code echoes a user message that carries only
the local command's output:

```json
{
  "type": "user",
  "message": {
    "role": "user",
    "content": "<local-command-stdout>Set model to `haiku (claude-haiku-5-5)`</local-command-stdout>"
  },
  "parent_tool_use_id": null,
  "uuid": "…",
  "isReplay": true
}
```

The v2 session read every top-level echo as a prompt that Claude Code
took in, and opened a turn for it. A model change runs no turn: no
`command_lifecycle` frame, `result` or `session_state_changed` follows
the echo, so nothing closed that turn.

Captured against the bundled CLI (adapter 0.104.1): `state_update
running` went out 18 ms after `session/set_config_option`, and no `idle`
followed in the next 75 seconds.

## Fix

An echo whose content is only local command output, and whose uuid is
not one of the session's pending prompts, no longer opens a turn. The
check is `isLocalCommandOutput` in `message-filters.ts`, next to the
helpers that already recognize these markers.

The pending-prompt exception keeps a client's own prompt working with a
CLI that sends no `command_lifecycle` frames, where the echo is the only
sign that the prompt was inserted.

## Scope

- v2 only. The v1 core takes its state from `session_state_changed` and
does not open turns at echoes.
- I captured a model change. Other local commands that Claude Code runs
outside a turn and echoes the same way are covered by the same check,
but I did not capture them.

## Testing

- New case in `src/tests/v2/v2.test.ts` sends the echo as the CLI sent
it. Without the fix it fails with `expected [ 'running' ] to deeply
equal []`.
- Reran the capture against the bundled CLI with the fix: no `running`
after either model change, and the next prompt ran `running` then
`idle`.
- `npm run check` and `npm run test:run` pass.
2026-11-10 18:49:50 +00:00
Ben Brandt
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.
2026-11-10 18:28:46 +00:00
106 changed files with 3459 additions and 678 deletions

View File

@@ -1,3 +1,3 @@
{
".": "0.104.1"
".": "0.106.0"
}

View File

@@ -1,5 +1,36 @@
# Changelog
## [0.106.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.105.0...v0.106.0) (2026-11-10)
### Features
* report the auth detail as the account's description ([#1591](https://github.com/agentclientprotocol/claude-agent-acp/issues/1591)) ([f6d6429](https://github.com/agentclientprotocol/claude-agent-acp/commit/f6d64291876177f9f593415abfc7e50eb7e7ff56))
* serve session/options and initial config options ([#1592](https://github.com/agentclientprotocol/claude-agent-acp/issues/1592)) ([b1763c7](https://github.com/agentclientprotocol/claude-agent-acp/commit/b1763c750aef83fd05ce16901ff1a0d9b4b90234))
### Bug Fixes
* honour the client's settings option in idle compaction ([#1555](https://github.com/agentclientprotocol/claude-agent-acp/issues/1555)) ([b73ee49](https://github.com/agentclientprotocol/claude-agent-acp/commit/b73ee491c8f60a1584a95a7f03557f356e1a991c))
* replay the messages before each compaction on ACP v1 and v2 ([#1430](https://github.com/agentclientprotocol/claude-agent-acp/issues/1430)) ([626015b](https://github.com/agentclientprotocol/claude-agent-acp/commit/626015bf81fd34f952406ae77bf60feb9ca39a16))
* stop the release executable from loading the project's .env and bunfig.toml ([#1589](https://github.com/agentclientprotocol/claude-agent-acp/issues/1589)) ([4a3ff6a](https://github.com/agentclientprotocol/claude-agent-acp/commit/4a3ff6a1eb16075b96676616ab2bbb98694a4d24))
* update to @agentclientprotocol/sdk 2.6.0 ([#1590](https://github.com/agentclientprotocol/claude-agent-acp/issues/1590)) ([9fca22a](https://github.com/agentclientprotocol/claude-agent-acp/commit/9fca22a03bdec5dc265bfdc42df316fb286a8142))
## [0.105.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.104.1...v0.105.0) (2026-11-10)
### Features
* serve session/set_title on ACP v1 and v2 ([#1585](https://github.com/agentclientprotocol/claude-agent-acp/issues/1585)) ([a4a8534](https://github.com/agentclientprotocol/claude-agent-acp/commit/a4a8534c23b08cdfd7f656d25f9ab16068fd8ffd))
* serve session/status on v1 and v2 ([#1584](https://github.com/agentclientprotocol/claude-agent-acp/issues/1584)) ([6bc2e05](https://github.com/agentclientprotocol/claude-agent-acp/commit/6bc2e058cb27624b4307462ac46167535af400d1))
### Bug Fixes
* include inline model settings in the model picker ([#1488](https://github.com/agentclientprotocol/claude-agent-acp/issues/1488)) ([36cb674](https://github.com/agentclientprotocol/claude-agent-acp/commit/36cb6745897bf9dabcb967b54233c42b860436b3))
* keep a v2 session idle when the model changes between turns ([#1586](https://github.com/agentclientprotocol/claude-agent-acp/issues/1586)) ([f5d7916](https://github.com/agentclientprotocol/claude-agent-acp/commit/f5d79167a649737bdeb1ce3b55ad449a34bb9d47))
* report Claude Code's API error message as a notice in v2 ([#1582](https://github.com/agentclientprotocol/claude-agent-acp/issues/1582)) ([99cc1f3](https://github.com/agentclientprotocol/claude-agent-acp/commit/99cc1f324586fad07f1da14bb9764cd16c64e920))
## [0.104.1](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.104.0...v0.104.1) (2026-11-10)

View File

@@ -140,7 +140,8 @@ for v2 is negotiated down to v1. With `v2`, the adapter rejects the `initialize`
On v2 the adapter serves the session baseline (`session/new`, `session/list`, `session/resume`
with replay, `session/close`, `session/prompt`, `session/cancel`), `session/delete`,
`session/fork`, `session/status`, additional directories, config options (the mode is one), plans, notices,
`session/fork`, `session/status`, `session/options` (with initial `configOptions` on `session/new` and
`session/resume`), `session/set_title`, additional directories, config options (the mode is one), plans, notices,
compaction, display terminals for commands, permission requests, elicitation, providers, auth, and
[subagents](#subagent-sessions). Steering is v1 only for now.

View File

@@ -228,7 +228,7 @@ The adapter keeps them where they are.
## Auth source display
The connection-scoped `_auth/status_update` continues to report agent-owned authentication, not client-owned `providers/set` routing. The adapter also reports the same identity through the ACP `auth/status` method and `auth/status_update` notification (advertised as `auth.status`), with `kind`, `detail` and `plan` under the account's `_meta.claudeCode`. The account's `label` also carries a `detail` in parentheses, e.g. `Anthropic API key (ANTHROPIC_API_KEY)`. Each notification is sent when its own payload changed. `auth/status` answers with the identity last reported and starts a new read, whose change arrives as a notification. When the active key source is `apiKeyHelper`, the adapter reads the effective helper command through SDK `resolveSettings` / the session's `SettingsManager`, including programmatic settings overrides.
The connection-scoped `_auth/status_update` continues to report agent-owned authentication, not client-owned `providers/set` routing. The adapter also reports the same identity through the ACP `auth/status` method and `auth/status_update` notification (advertised as `auth.status`), with `kind`, `detail` and `plan` under the account's `_meta.claudeCode`. The account's `description` carries the `detail` too, e.g. `label: "Anthropic API key"` with `description: "ANTHROPIC_API_KEY"`. Each notification is sent when its own payload changed. `auth/status` answers with the identity last reported and starts a new read, whose change arrives as a notification. When the active key source is `apiKeyHelper`, the adapter reads the effective helper command through SDK `resolveSettings` / the session's `SettingsManager`, including programmatic settings overrides.
A command containing the whole word `central` and the word sequence `proxy start` is displayed as:
@@ -1408,6 +1408,8 @@ Each row carries `_meta.jetbrains.air` with flat row fields of the RFDs. `archiv
`_session/rename { "sessionId": "…", "title": "…" }` returns `{}`.
ACP's `session/set_title` renames the same way for every client, on v1 and v2, without `sessionIndex`, and returns `{ "title": "…" }`: the title as `session/list` shows it to the client. On v1 an AIR client gets it without the archive prefix, and every other client, and every v2 client, with it.
- A blank title is `-32602`. The adapter collapses whitespace and keeps the first 200 characters, trimmed, as the CLI does. An `[archived] ` prefix of the requested title is dropped: a rename never changes the archive state.
- The title is stored as the title records of [Archive](#archive): a `custom-title` and an `agent-name` record. An archived session keeps its `[archived] ` prefix (and the title is cut to 200 characters with it), so a rename does not unarchive it.
- A session that runs here on a CLI without the `rename_session` control request is refused with `-32600`: its CLI holds the title and would write it back.

12
package-lock.json generated
View File

@@ -1,15 +1,15 @@
{
"name": "@agentclientprotocol/claude-agent-acp",
"version": "0.104.1",
"version": "0.106.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@agentclientprotocol/claude-agent-acp",
"version": "0.104.1",
"version": "0.106.0",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "2.5.0",
"@agentclientprotocol/sdk": "2.6.0",
"@anthropic-ai/claude-agent-sdk": "0.3.322",
"diff": "9.0.0",
"zod": "4.6.7"
@@ -38,9 +38,9 @@
}
},
"node_modules/@agentclientprotocol/sdk": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-2.5.0.tgz",
"integrity": "sha512-MFlXeIuwN6nZSK45SAsSnVjbLgQ2r4fnthoF1lQoGJP3PdaPRpOZeCF2Dds8S4cN8yZP5QbxB1bkJccGskmcYw==",
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-2.6.0.tgz",
"integrity": "sha512-3FtL6MkAV5F9qxg0FQV8wmBszIm2xuVPoDWOILIFSDQ+Vq3Twdh7Cw6Hk2GOPUbevgrTUpUAqZXQy7Geaj79mw==",
"license": "Apache-2.0",
"peerDependencies": {
"@modelcontextprotocol/server": "^2.4.0",

View File

@@ -3,7 +3,7 @@
"publishConfig": {
"access": "public"
},
"version": "0.104.1",
"version": "0.106.0",
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
"main": "dist/lib.js",
"types": "dist/lib.d.ts",
@@ -68,7 +68,7 @@
"node": ">=22"
},
"dependencies": {
"@agentclientprotocol/sdk": "2.5.0",
"@agentclientprotocol/sdk": "2.6.0",
"@anthropic-ai/claude-agent-sdk": "0.3.322",
"diff": "9.0.0",
"zod": "4.6.7"

View File

@@ -74,9 +74,24 @@ try {
process.exit(1);
}
// By default a compiled executable loads `.env`, `.env.local` and `bunfig.toml`
// from its working directory at startup. That directory is the user's project:
// its dotenv values would reach Claude and every Bash command as real
// environment variables (overriding the project's own `.env.test`), and a
// `bunfig.toml` that fails to parse would stop the agent from starting.
const bun = spawnSync(
"bun",
["build", "src/binary-entry.ts", "--compile", "--target", target, "--outfile", outfile],
[
"build",
"src/binary-entry.ts",
"--compile",
"--no-compile-autoload-dotenv",
"--no-compile-autoload-bunfig",
"--target",
target,
"--outfile",
outfile,
],
{ cwd: repoRoot, stdio: "inherit", shell: process.platform === "win32" },
);
if (bun.error) {

View File

@@ -58,8 +58,9 @@ function check(ok, label, detail) {
// ---------------------------------------------------------------- basic
function runSync(args, [cmd, ...cmdArgs] = [command, ...commandArgs]) {
function runSync(args, [cmd, ...cmdArgs] = [command, ...commandArgs], cwd) {
const result = spawnSync(cmd, [...cmdArgs, ...args], {
cwd,
encoding: "utf8",
timeout: 60_000,
});
@@ -101,6 +102,26 @@ if (positionals.length === 0) {
"without claude next to it, says the CLI is missing",
missing.stdout || missing.error,
);
// Started in a project directory, the executable must not read the
// project's dotenv files or bunfig.toml. Node stands in for `claude`, to
// print the environment the CLI is spawned with.
copyFileSync(process.execPath, join(scratch, `claude${exe}`));
const project = join(scratch, "project");
mkdirSync(project);
writeFileSync(join(project, ".env"), "ACP_SMOKE_DOTENV=leaked\n");
writeFileSync(join(project, ".env.local"), "ACP_SMOKE_DOTENV_LOCAL=leaked\n");
writeFileSync(join(project, "bunfig.toml"), "= not toml\n");
const probe = runSync(
["--cli", "-p", "[process.env.ACP_SMOKE_DOTENV, process.env.ACP_SMOKE_DOTENV_LOCAL].join()"],
[alone],
project,
);
check(
probe.stdout === ",",
"ignores .env, .env.local and bunfig.toml in the working directory",
probe.stdout || probe.error,
);
} finally {
rmSync(scratch, { recursive: true, force: true });
}

View File

@@ -104,10 +104,10 @@ export type AcpAuthStatus = Pick<AuthStatusResponse, "state" | "accounts">;
* - No identity read yet (`undefined`): `unknown`, no accounts.
* - `kind: "none"`: `unauthenticated`, no accounts.
* - Any other kind: `authenticated`, with the identity as the one account,
* marked `active`. The account has no field for `detail`, so its label
* carries it in parentheses, e.g. "Anthropic API key (ANTHROPIC_API_KEY)",
* for clients that show only the standard fields. `_meta.claudeCode`
* carries the `kind`, `detail` and `plan` on their own.
* marked `active`. `detail` is the account's `description`, e.g. label
* "Anthropic API key" with description "ANTHROPIC_API_KEY".
* `_meta.claudeCode` carries the `kind` and `plan`, and the `detail` again
* for clients that read it there.
*
* `--hide-claude-auth` does not change the payload: it reports the login the
* CLI holds, also when the subscription guard refuses to use it.
@@ -119,8 +119,8 @@ export function toAcpAuthStatus(status: AuthStatus | undefined): AcpAuthStatus {
if (status.kind === "none") {
return { state: "unauthenticated" };
}
const label = status.detail ? `${status.label} (${status.detail})` : status.label;
const account: AuthAccount = { label, active: true };
const account: AuthAccount = { label: status.label, active: true };
if (status.detail) account.description = status.detail;
if (status.account?.email) account.email = status.account.email;
if (status.account?.organization) account.organization = status.account.organization;
account._meta = {

View File

@@ -6,11 +6,10 @@ import {
import {
forkSession as forkClaudeSession,
getSessionMessages,
importSessionToStore,
type SessionStore,
type SessionStoreEntry,
} from "@anthropic-ai/claude-agent-sdk";
import { createHash } from "node:crypto";
import { loadFullSessionHistory } from "./resumed-session.js";
import { assistantMessageText } from "./session-failure-extension.js";
import { SessionTiming } from "./session-timing.js";
@@ -69,19 +68,6 @@ function forkPointMessageIdCandidates(messageId: string): string[] {
return protocolMessageId === messageId ? [messageId] : [messageId, protocolMessageId];
}
async function loadFullSessionHistory(sessionId: string): Promise<SessionStoreEntry[]> {
// getSessionMessages returns only the active parentUuid chain. The import keeps inactive branches that AIR can still reference.
const entries: SessionStoreEntry[] = [];
const store: SessionStore = {
append: async (_key, batch) => {
entries.push(...batch);
},
load: async () => null,
};
await importSessionToStore(sessionId, store, { includeSubagents: false });
return entries;
}
function assistantGroups(
entries: SessionStoreEntry[],
messageIdForGrouping: ForkSessionDependencies["messageIdForGrouping"],
@@ -235,6 +221,7 @@ async function forkTranscript(
.find((message) => dependencies.messageIdForGrouping(message) === candidateId)?.uuid,
)
.find(Boolean);
// getSessionMessages returns only the active parentUuid chain. The full history keeps inactive branches that AIR can still reference.
const fullHistoryUuid = messageUuid
? undefined
: resolveFromFullHistory(

View File

@@ -5,7 +5,8 @@ export type IdleCompactionHost = {
isBusy(): boolean;
/** Context size after the last main-thread request, or undefined if unknown. */
contextTokens(): number | undefined;
/** Current merged Claude Code settings. */
/** Current Claude Code settings, the client's `settings` option included
* (`SettingsManager.getCompactionSettings`). */
settings(): {
idleCompaction?: boolean;
autoCompactEnabled?: boolean;

View File

@@ -0,0 +1,63 @@
/**
* The unstable "Session options" RFD, as both cores serve it: `session/options`
* answers with what a session would start with, and a setup request's
* `configOptions` set the initial values of the session's config options.
*/
import { RequestError } from "@agentclientprotocol/sdk/unstable";
import type { Logger } from "./logger.js";
/**
* What a core advertises as `options` in its session capabilities:
* `session/options`, and `configOptions` on its setup requests.
*/
export const SESSION_OPTIONS_CAPABILITY = {};
const INVALID_PARAMS = RequestError.invalidParams().code;
/**
* The entries of a setup request's `configOptions`. Omitted and `null` are no
* entries. Two entries with the same `configId` fail with `-32602`. Call it
* before the session starts, so a refused request starts none.
*/
export function initialConfigOptions<E extends { configId: string }>(
entries: readonly E[] | null | undefined,
): readonly E[] {
const seen = new Set<string>();
for (const { configId } of entries ?? []) {
if (seen.has(configId)) {
throw RequestError.invalidParams(
{ configId },
`\`configOptions\` sets ${configId} more than once. Send one entry for each config option.`,
);
}
seen.add(configId);
}
return entries ?? [];
}
/**
* Sets each entry with `set`, in list order. `set` checks an entry against the
* options as the entries before it left them, so a model entry decides which
* effort levels a later entry can pick.
*
* An entry that `set` refuses is skipped, and the entries after it still
* apply. The option keeps its value, which the setup response reports:
*
* - `-32602`: an unknown `configId`, a value of another type than the
* option's, or a value that the option does not offer.
* - Any other failure, such as a model that Claude Code refuses. It is logged.
*/
export async function applyInitialConfigOptions<E extends { configId: string }>(
entries: readonly E[],
set: (entry: E) => Promise<unknown>,
logger: Logger,
): Promise<void> {
for (const entry of entries) {
try {
await set(entry);
} catch (error) {
if (error instanceof RequestError && error.code === INVALID_PARAMS) continue;
logger.error(`Could not set the initial value of config option ${entry.configId}:`, error);
}
}
}

View File

@@ -87,6 +87,22 @@ function hasMarkerTag(text: string, tag: string): boolean {
return markerTagText(text, tag) !== undefined;
}
/**
* Whether the content of a user message is only the output of a local
* command, such as the `<local-command-stdout>Set model to …</local-command-stdout>`
* that Claude Code echoes when the model changes.
*/
export function isLocalCommandOutput(content: unknown): boolean {
if (typeof content !== "string") return false;
if (
!hasMarkerTag(content, "local-command-stdout") &&
!hasMarkerTag(content, "local-command-stderr")
) {
return false;
}
return stripMarkerTags(content).trim() === "";
}
function commandInvocationFromMarkerOnlyText(text: string): string | null {
if (hasMarkerTag(text, "local-command-stdout") || hasMarkerTag(text, "local-command-stderr")) {
return null;

View File

@@ -1,6 +1,13 @@
import { getSessionMessages, type SessionMessage } from "@anthropic-ai/claude-agent-sdk";
import {
getSessionMessages,
importSessionToStore,
type SessionMessage,
type SessionStore,
type SessionStoreEntry,
} from "@anthropic-ai/claude-agent-sdk";
import { access, open, readdir } from "node:fs/promises";
import path from "node:path";
import { isCompactSummaryMessage } from "./context-compaction.js";
import { claudeConfigDir } from "./paths.js";
import { SessionTiming } from "./session-timing.js";
@@ -68,6 +75,97 @@ export async function readResumedSession(
}
}
/**
* Read the messages of a session from its first prompt on, given the
* `getSessionMessages` chain of the session.
*
* `getSessionMessages` follows `parentUuid` links. Claude Code writes each
* `compact_boundary` with `parentUuid: null`, so after a compaction the chain
* starts at its summary. The boundary keeps the last message before it in
* `logicalParentUuid`. For a chain that starts at a summary, this links each
* boundary to that message and reads the transcript through
* `getSessionMessages` again. For a session compacted twice, the result has
* both summaries and the messages before, between and after them. The result
* ends at the last message of `messages`. Any other chain is returned as it
* is, and so is the chain when the read fails or when its result does not
* reach that last message (e.g. the transcript changed between the reads).
*
* `dir` is the project directory of the first read: both reads then resolve
* the same transcript file. Without it, all project directories are searched.
*
* The preserved-message metadata of each boundary is dropped. With it,
* `getSessionMessages` moves the messages that a compaction kept to after the
* summary, and the relinked boundary then leads from them back to the summary:
* the chain loops and ends there. Without it, the kept messages stay where
* they were said, before the boundary.
*/
export async function readHistoryAcrossCompactions(
sessionId: string,
messages: SessionMessage[],
logger?: ResumeLogger,
dir?: string,
): Promise<SessionMessage[]> {
if (!isCompactSummaryMessage(messages[0])) return messages;
try {
const entries = (await loadFullSessionHistory(sessionId, dir)).map(linkCompactBoundary);
const history = await getSessionMessages(sessionId, {
dir,
sessionStore: { append: async () => {}, load: async () => entries },
});
const leaf = messages.at(-1)?.uuid;
const end = history.findIndex((message) => message.uuid === leaf);
if (end < 0) {
logger?.error(
`The history before the last compaction of session ${sessionId} does not reach message ${leaf}: replaying the messages after it`,
);
return messages;
}
return history.slice(0, end + 1);
} catch (error) {
logger?.error(
`Failed to read the history before the last compaction of session ${sessionId}:`,
error,
);
return messages;
}
}
/** Every record of a session's transcript, including the inactive branches
* and the history before a compaction that `getSessionMessages` leaves out.
* Without `dir`, all project directories are searched. */
export async function loadFullSessionHistory(
sessionId: string,
dir?: string,
): Promise<SessionStoreEntry[]> {
const entries: SessionStoreEntry[] = [];
const store: SessionStore = {
append: async (_key, batch) => {
entries.push(...batch);
},
load: async () => null,
};
await importSessionToStore(sessionId, store, { dir, includeSubagents: false });
return entries;
}
/** The record unchanged, or a compaction boundary linked to the message before it. */
function linkCompactBoundary(entry: SessionStoreEntry): SessionStoreEntry {
if (
entry.type !== "system" ||
entry.subtype !== "compact_boundary" ||
typeof entry.logicalParentUuid !== "string"
) {
return entry;
}
const compactMetadata: Record<string, unknown> =
typeof entry.compactMetadata === "object" && entry.compactMetadata !== null
? { ...entry.compactMetadata }
: {};
delete compactMetadata.preservedSegment;
delete compactMetadata.preservedMessages;
return { ...entry, parentUuid: entry.logicalParentUuid, compactMetadata };
}
/** What a resume restores from the end of the local transcript. */
export type ResumedTail = {
model?: string;

View File

@@ -1,6 +1,7 @@
import type { ClientCapabilities, SessionNotification } from "@agentclientprotocol/sdk/unstable";
import {
getSessionMessages,
type SDKAPIRetryMessage,
type SDKAssistantMessageError,
USAGE_LIMIT_ERROR_PREFIXES,
} from "@anthropic-ai/claude-agent-sdk";
@@ -446,6 +447,84 @@ export class SessionFailureController {
}
}
/** Formats a wait in whole seconds, at least 1s: `8000` → `"8s"`. */
function retrySeconds(ms: number): string {
return `${Math.max(1, Math.round(ms / 1000))}s`;
}
/** The wait before the next attempt: `0` → `"now"`, `8000` → `"in 8s"`, and
* from 120 s whole minutes rounded up: `1_800_000` → `"in 30m"`. The CLI sends
* `0` for a `no_response` retry and minutes for a background rate-limit wait. */
function nextAttemptDelay(ms: number): string {
const seconds = Math.round(ms / 1000);
if (seconds <= 0) return "now";
if (seconds < 120) return `in ${seconds}s`;
return `in ${Math.ceil(ms / 60_000)}m`;
}
/** Plain-English names of the SDK error kinds. `unknown` has none: it adds
* nothing to the HTTP status or the connection error. */
const RETRY_ERROR_KIND_LABELS: Record<Exclude<SDKAssistantMessageError, "unknown">, string> = {
authentication_failed: "authentication failed",
oauth_org_not_allowed: "organization not allowed",
account_on_hold: "account on hold",
verification_required: "verification required",
billing_error: "billing error",
rate_limit: "rate limited",
overloaded: "overloaded",
invalid_request: "invalid request",
model_not_found: "model not found",
server_error: "server error",
max_output_tokens: "output token limit",
cloud_credential_error: "cloud credential error",
};
/** The `api_retry` message fields the warning reads. `error` is optional
* because an older CLI can omit it; the warning then treats it as `unknown`. */
type ApiRetryWarningInput = Pick<
SDKAPIRetryMessage,
"attempt" | "max_retries" | "error_status" | "retry_delay_ms" | "no_response"
> & { error?: SDKAssistantMessageError };
/** The cause of a retry. A null `error_status` with a kind is not always a
* lost connection: the CLI maps an SSE `overloaded_error` event after HTTP 200
* to `overloaded` with no status. */
function apiRetryCause(message: ApiRetryWarningInput): string {
const kind = message.error ?? "unknown";
const label =
kind === "unknown"
? undefined
: (RETRY_ERROR_KIND_LABELS[kind] ?? String(kind).replaceAll("_", " "));
if (message.error_status !== null) {
return `HTTP ${message.error_status}${label ? ` (${label})` : ""}.`;
}
return label ? `No HTTP status (${label}).` : "Connection error: no HTTP response.";
}
/** The title and the `details` of an `api_retry` warning. The title says the
* attempt and, for a `no_response` retry, how long the CLI waited and will
* wait. The details say the cause and the wait before the next attempt, e.g.
* "HTTP 529 (overloaded). Next attempt in 8s." The details do not repeat the
* `no_response` wait. */
export function apiRetryWarning(message: ApiRetryWarningInput): {
title: string;
details: string;
} {
// `no_response` (SDK 0.3.261+): the API sent no response headers
// within the first-byte window, so this retry waits longer for
// them. Say so — "attempt 1 of 1" alone reads like a final
// failure, and the wait is what the user is about to sit through.
const noResponse = message.no_response
? ` No response after ${retrySeconds(message.no_response.waited_ms)}; waiting up to ${retrySeconds(message.no_response.retry_wait_ms)}.`
: "";
const title =
message.error_status === null
? `Reconnecting to Claude, attempt ${message.attempt} of ${message.max_retries}.${noResponse}`
: `Retrying Claude, attempt ${message.attempt} of ${message.max_retries}.${noResponse}`;
const delay = nextAttemptDelay(message.retry_delay_ms);
return { title, details: `${apiRetryCause(message)} Next attempt ${delay}.` };
}
export function providerFailureCategory(
errorKind?: SDKAssistantMessageError,
isUsageLimit = false,

View File

@@ -28,16 +28,9 @@ export function fastModeStateEnabled(state: FastModeState): boolean {
return state !== "off";
}
/** User-facing explanations for the SDK's `fast_mode_disabled_reason` values
* that a user can act on (or at least wants to know about). Deliberately
* partial — the omitted reasons are not worth surfacing:
* - `sdk_opt_in_required`: every SDK session starts here (the toggle IS the
* opt-in), so it describes the default, not a problem.
* - `preference`: the user turned Fast mode off themselves.
* - `pending`: eligibility is still resolving; the next report supersedes it.
* - `unknown`: nothing meaningful to say.
* Unknown future reasons fall through the same way (open set — the SDK's docs
* say to ignore values you don't handle). */
/** User-facing explanations for the SDK's `fast_mode_disabled_reason` values.
* Every reason is explained except the two in {@link FAST_MODE_UNEXPLAINED_REASONS};
* a code missing here gets the generic text from {@link explainFastModeDisabledReason}. */
const FAST_MODE_UNAVAILABLE_EXPLANATIONS: Partial<Record<FastModeDisabledReason, string>> = {
free: "not available on the free plan",
extra_usage_disabled: "requires extra usage to be enabled for this account",
@@ -45,17 +38,38 @@ const FAST_MODE_UNAVAILABLE_EXPLANATIONS: Partial<Record<FastModeDisabledReason,
not_first_party: "not available on this API provider",
disabled_by_env: "disabled by environment configuration",
network_error: "eligibility could not be verified (network error)",
preference: "not enabled for your organization or API gateway",
unknown: "currently unavailable",
};
/** Reasons that are never explained:
* - `sdk_opt_in_required`: every SDK session starts here (the toggle IS the
* opt-in), so it describes the default, not a problem.
* - `pending`: eligibility is still resolving; the next report supersedes it. */
const FAST_MODE_UNEXPLAINED_REASONS: ReadonlySet<FastModeDisabledReason> = new Set([
"sdk_opt_in_required",
"pending",
]);
/** The user-facing explanation of a `fast_mode_disabled_reason`: `undefined`
* for no reason, `sdk_opt_in_required` or `pending`; the table text when there
* is one; else a generic text naming the code (a reason a newer SDK added). */
export function explainFastModeDisabledReason(
reason: FastModeDisabledReason | undefined,
): string | undefined {
if (reason === undefined || FAST_MODE_UNEXPLAINED_REASONS.has(reason)) return undefined;
return FAST_MODE_UNAVAILABLE_EXPLANATIONS[reason] ?? `not available (${reason})`;
}
/** Normalize an SDK-reported `fast_mode_disabled_reason` to the one we retain:
* a reason we have an explanation for, else `undefined`. Keeping only
* explainable reasons means state comparisons (see `fastModeStateChange`) track
* exactly what the user can see, so routine reports like
* `sdk_opt_in_required` never churn the config option. */
* a reason {@link explainFastModeDisabledReason} explains (including codes a
* newer SDK adds), else `undefined`. State comparisons (see
* `fastModeStateChange`) then track exactly what the user can see, so
* `sdk_opt_in_required` and `pending` never churn the config option. */
export function normalizeFastModeDisabledReason(
reason: FastModeDisabledReason | undefined,
): FastModeDisabledReason | undefined {
return reason && FAST_MODE_UNAVAILABLE_EXPLANATIONS[reason] ? reason : undefined;
return explainFastModeDisabledReason(reason) !== undefined ? reason : undefined;
}
/** Build the Fast mode config option. When the Client supports boolean config
@@ -73,9 +87,7 @@ export function createFastModeConfigOption(
useBooleanOption: boolean,
disabledReason?: FastModeDisabledReason,
): SessionConfigOption {
const explanation = enabled
? undefined
: disabledReason && FAST_MODE_UNAVAILABLE_EXPLANATIONS[disabledReason];
const explanation = enabled ? undefined : explainFastModeDisabledReason(disabledReason);
const base = {
id: FAST_MODE_CONFIG_ID,
name: "Fast mode",
@@ -141,15 +153,17 @@ export type FastModeOptionState = {
* off), or the same state with the same explainable reason.
*
* `reason` is the SDK's `fast_mode_disabled_reason`, reported alongside the
* state. Only explainable reasons are retained (see
* {@link normalizeFastModeDisabledReason}), so a routine
* `sdk_opt_in_required` report on every turn's result can't churn the
* option, while a real blocker updates the description even when the
* state. Every reason except `sdk_opt_in_required` and `pending` is retained
* (see {@link normalizeFastModeDisabledReason}; unknown future codes get the
* generic text), so `sdk_opt_in_required` on every turn's result can't churn
* the option, while a real blocker updates the description even when the
* toggle's own value is unchanged.
*
* `notice` is set when the user had Fast mode on and the SDK turned it off
* for an explainable reason: the description carries the same explanation,
* but a toggle silently snapping back is worth saying out loud once. */
* with a retained reason: the description carries the same explanation, but a
* toggle silently snapping back is worth saying out loud once. Off with no
* reason, `sdk_opt_in_required` or `pending` stays quiet: a `result` can carry
* the turn-start snapshot, so a notice there would falsely say "turned off". */
export function fastModeStateChange(
current: { enabled: boolean; disabledReason?: FastModeDisabledReason },
state: FastModeState | undefined,
@@ -163,8 +177,7 @@ export function fastModeStateChange(
// so it can't decorate the option the next time fast mode goes off.
const disabledReason = enabled ? undefined : normalizeFastModeDisabledReason(reason);
if (enabled === current.enabled && disabledReason === current.disabledReason) return undefined;
const explanation =
disabledReason !== undefined ? FAST_MODE_UNAVAILABLE_EXPLANATIONS[disabledReason] : undefined;
const explanation = explainFastModeDisabledReason(disabledReason);
if (!current.enabled || enabled || explanation === undefined) {
return { enabled, disabledReason };
}

View File

@@ -40,7 +40,7 @@
import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";
import { TASK_NOTIFICATION_TAG, taskNotificationsOf } from "../../task-notifications.js";
import { TASK_NOTIFICATION_TAG, taskNotificationsOf } from "../task-notifications.js";
export type BackgroundTasks = {
/** The ids of the tasks that started and did not end, in start order. */

View File

@@ -49,7 +49,7 @@ import {
type LiveSessionRegistry,
type LiveSnapshot,
} from "./live-registry.js";
import { isSessionId, projectsRoot } from "../../paths.js";
import { isSessionId, projectsRoot } from "../paths.js";
import { isExactProjectDir, projectDirMatches } from "./project-dirs.js";
import { type IndexRow, type SessionIndex, type TranscriptCandidate } from "./session-index.js";
import { changeSignature, sessionInfoOf } from "./session-info.js";

View File

@@ -23,7 +23,7 @@ import { execFile } from "node:child_process";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { claudeConfigDir } from "../../paths.js";
import { claudeConfigDir } from "../paths.js";
import { errorCode } from "./project-dirs.js";
const SNAPSHOT_TTL_MS = 5_000;

115
src/session-index/rename.ts Normal file
View File

@@ -0,0 +1,115 @@
/**
* Client renames of a session: `session/set_title` (v1 and v2) and AIR's
* `_session/rename` (v1). Both cores call {@link renameSession}, which stores
* the title in every transcript of the session (see
* {@link SessionIndexService.retitle}) and, for a session loaded on the
* connection, makes it the client title that no generated title replaces.
*/
import { RequestError } from "@agentclientprotocol/sdk/unstable";
import type { Query } from "@anthropic-ai/claude-agent-sdk";
import type { SessionTitles, TitledSession } from "../session-titles.js";
import { storedTitle } from "./archive-title.js";
import {
renameTo,
type RenameSessionRequest,
type RetitleOptions,
type SessionIndexService,
type TitleChange,
} from "./service.js";
/** A session loaded on the connection, and its title state. */
export interface RetitledSession {
readonly session: TitledSession;
readonly titles: Pick<
SessionTitles,
"setExplicitTitle" | "storedTitle" | "rememberStoredTitle" | "shownTitle"
>;
}
type RenamingQuery = Query & {
renameSession?: (title: string, sessionId?: string) => Promise<void>;
};
/**
* Names a session; no generated title replaces it. An archived session stays
* archived. `loaded` is the session loaded on this connection, if any;
* `clientTitle` turns a stored title into the one the client shows (an AIR
* client's drops the archive prefix). Returns that title, which the loaded
* session also publishes as a `session_info_update`.
*
* A session that is not loaded and has no transcript is unknown (`-32002`).
*/
export async function renameSession(
index: SessionIndexService,
request: RenameSessionRequest,
loaded: RetitledSession | undefined,
clientTitle: (stored: string) => string = (stored) => stored,
): Promise<string> {
const { sessionId } = request;
// As the CLI keeps it, and never with the archive prefix: a rename does
// not change the archive state.
const title = storedTitle(request.title, false, sessionId);
// The title as stored: an archived one is cut with its prefix.
const shown = (stored: string | undefined) => clientTitle(stored ?? title);
const stored = await retitleSession(index, sessionId, renameTo(title), shown, loaded, {
mayBeUnwritten: true,
sidecar: "always",
});
return shown(stored);
}
/**
* Retitles a session (see {@link SessionIndexService.retitle}) and returns
* the title stored. A session that runs here is titled through its CLI
* (`rename_session`). A loaded session's title state takes the change as a
* client title, so no generated title replaces it; `publish` is the title it
* then publishes.
*/
export async function retitleSession(
index: SessionIndexService,
sessionId: string,
change: TitleChange,
publish: Parameters<SessionTitles["setExplicitTitle"]>[0],
loaded: RetitledSession | undefined,
options: Pick<RetitleOptions, "mayBeUnwritten" | "sidecar">,
): Promise<string | undefined> {
let stored: string | undefined;
// Decided once a title generation in flight has ended: the query may
// have closed meanwhile.
const persist = async () => {
const session = loaded?.session;
const query = session?.query as RenamingQuery | undefined;
// A closed session's CLI is gone here: only a running query titles it.
const running = session !== undefined && !session.queryClosed;
if (running && typeof query?.renameSession !== "function") {
// Its CLI holds the title and would write it back over ours.
throw RequestError.invalidRequest(
{ sessionId },
"The Claude Code CLI of this session cannot change its title while it runs.",
);
}
const live = running
? {
cwd: session.cwd,
rename: (title: string) => query!.renameSession!(title, sessionId),
stored: () => loaded!.titles.storedTitle,
remember: (title: string) => loaded!.titles.rememberStoredTitle(title),
shown: loaded!.titles.shownTitle,
}
: undefined;
stored = await index.retitle(sessionId, change, {
...options,
// Without its CLI, a session needs its transcript.
mayBeUnwritten: live !== undefined && options.mayBeUnwritten,
live,
});
return stored;
};
if (loaded) {
await loaded.titles.setExplicitTitle(publish, persist);
} else {
await persist();
}
return stored;
}

View File

@@ -18,8 +18,8 @@ import {
type SessionInfo,
} from "@agentclientprotocol/sdk/unstable";
import { deleteSession as sdkDeleteSession } from "@anthropic-ai/claude-agent-sdk";
import { airExtensionMeta } from "../../air-extension.js";
import { sanitizeTitle } from "../../session-titles.js";
import { airExtensionMeta } from "../air-extension.js";
import { sanitizeTitle } from "../session-titles.js";
import type { OwnSessionState } from "./activity.js";
import {
effectiveTitle,
@@ -36,7 +36,7 @@ import {
type ListSubscriptionDeps,
} from "./list-subscriptions.js";
import { LiveSessionRegistry } from "./live-registry.js";
import { isSessionId } from "../../paths.js";
import { isSessionId } from "../paths.js";
import {
canonicalPath,
exists,
@@ -466,6 +466,18 @@ export type SessionIndexDeps = {
logError: (message: string, error: unknown) => void;
};
/** The transcript reads and writes of the session index, without its list
* or watcher: what a client without `sessionIndex` uses for
* `session/set_title`, AIR's archive on delete, and delete's ordering
* after them (see {@link SessionIndexService.exclusive}). */
export function transcriptWriter(logger: {
error: (message: string, error: unknown) => void;
}): SessionIndexService {
return new SessionIndexService({
logError: (message, error) => logger.error(`[session-index] ${message}:`, error),
});
}
export class SessionIndexService {
readonly index: SessionIndex;
readonly registry: LiveSessionRegistry;
@@ -541,8 +553,10 @@ export class SessionIndexService {
};
}
/** Runs `mutation` after the previous mutation of the session ended. */
private exclusive<T>(sessionId: string, mutation: () => Promise<T>): Promise<T> {
/** Runs `mutation` after the previous mutation of the session ended, such
* as a delete that does not go through {@link delete}, so that a retitle
* cannot append to a transcript the delete is removing. */
exclusive<T>(sessionId: string, mutation: () => Promise<T>): Promise<T> {
const previous = this.mutations.get(sessionId) ?? Promise.resolve();
const next = previous.catch(() => undefined).then(mutation);
const settled = next.catch(() => undefined);

View File

@@ -21,9 +21,9 @@
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { sanitizeTitle } from "../../session-titles.js";
import { sanitizeTitle } from "../session-titles.js";
import { effectiveTitle, visibleTitle } from "./archive-title.js";
import { isSessionId, projectsRoot } from "../../paths.js";
import { isSessionId, projectsRoot } from "../paths.js";
import {
canonicalPath,
encodeProjectPath,

View File

@@ -5,7 +5,7 @@
*/
import type { SessionInfo } from "@agentclientprotocol/sdk/unstable";
import { withAirMeta } from "../../air-extension.js";
import { withAirMeta } from "../air-extension.js";
import { deriveActivity, selectCost, type OwnSessionState } from "./activity.js";
import type { LiveRecord } from "./live-registry.js";
import type { IndexRow } from "./session-index.js";

View File

@@ -285,7 +285,16 @@ export class SessionModeManager<S extends SessionMode> {
return true;
}
async selectMode(sessionId: string, modeId: string): Promise<PermissionMode> {
/**
* Sets the mode on the query. `auto` on a model without Auto runs as its
* fallback, with the warning. With `deferWarning`, the warning waits for
* the next prompt (see {@link deferFallbackWarning}).
*/
async selectMode(
sessionId: string,
modeId: string,
deferWarning = false,
): Promise<PermissionMode> {
const session = this.requireOpenSession(sessionId);
const requestedMode = this.parseMode(modeId);
if (!this.isAvailable(session.modes.availableModes, requestedMode)) {
@@ -308,7 +317,8 @@ export class SessionModeManager<S extends SessionMode> {
}
if (effectiveMode !== requestedMode) {
await this.publishFallbackWarning(sessionId, session);
if (deferWarning) this.deferFallbackWarning(session);
else await this.publishFallbackWarning(sessionId, session);
}
return effectiveMode;
}
@@ -352,6 +362,16 @@ export class SessionModeManager<S extends SessionMode> {
}
}
/**
* Leaves the Auto fallback's warning for the next prompt
* (`autoModeFallbackWarningPending`), as a session's start does. For a
* fallback during session setup: the client cannot place an update of a
* session before the setup response names it.
*/
deferFallbackWarning(session: SessionMode): void {
if (!session.autoModeFallbackWarningShown) session.autoModeFallbackWarningPending = true;
}
async publishFallbackState(sessionId: string, session: SessionMode): Promise<void> {
await this.publishCurrent(sessionId, AUTO_MODE_FALLBACK);
await this.publishFallbackWarning(sessionId, session);

View File

@@ -37,6 +37,15 @@ export interface SettingsManagerOptions {
logger?: { log: (...args: any[]) => void; error: (...args: any[]) => void };
}
type ModelSelectionSettings = Pick<Settings, "availableModels" | "modelOverrides">;
/** The settings that decide whether Claude Code compacts (see `idle-compaction.ts`). */
export type CompactionSettings = {
idleCompaction?: boolean;
autoCompactEnabled?: boolean;
env?: Record<string, string>;
};
/**
* Manages Claude Code settings using the SDK's `resolveSettings` merge engine
* so the values we see match what `query()` would observe.
@@ -50,7 +59,9 @@ export class SettingsManager {
private cwd: string;
private effective: Settings = {};
private managedDeniedModels: string[] = [];
private managedModelSettings: ModelSelectionSettings = {};
private managedApiKeyHelper?: string;
private managedCompactionSettings: CompactionSettings = {};
private watchers: fs.FSWatcher[] = [];
private onChange?: () => void;
private logger: { log: (...args: any[]) => void; error: (...args: any[]) => void };
@@ -112,15 +123,36 @@ export class SettingsManager {
this.managedDeniedModels = resolved.sources
.filter((entry) => entry.source === "managed")
.flatMap((entry) => entry.settings.deniedModels ?? []);
this.managedModelSettings = Object.assign(
{},
...resolved.sources
.filter((entry) => entry.source === "managed")
.map((entry) => ({
availableModels: entry.settings.availableModels,
modelOverrides: entry.settings.modelOverrides,
})),
);
this.managedApiKeyHelper =
resolved.provenance.apiKeyHelper?.source === "managed"
? resolved.effective.apiKeyHelper
: undefined;
const managed = resolved.sources
.filter((entry) => entry.source === "managed")
.map((entry) => entry.settings);
this.managedCompactionSettings = {
idleCompaction: managed.findLast((tier) => tier.idleCompaction !== undefined)
?.idleCompaction,
autoCompactEnabled: managed.findLast((tier) => tier.autoCompactEnabled !== undefined)
?.autoCompactEnabled,
env: Object.assign({}, ...managed.map((tier) => tier.env)),
};
} catch (error) {
this.logger.error("Failed to resolve settings:", error);
this.effective = {};
this.managedDeniedModels = [];
this.managedModelSettings = {};
this.managedApiKeyHelper = undefined;
this.managedCompactionSettings = {};
}
}
@@ -193,6 +225,51 @@ export class SettingsManager {
);
}
/** Model selection settings including the inline SDK settings tier. */
getModelSelectionSettings(override?: Settings): ModelSelectionSettings {
// The SDK unions non-managed allowlists, but a managed list owns the
// whole allowlist. Keep that policy when adding
// models the SDK did not include in its initialization result.
const lists = [this.effective.availableModels, override?.availableModels].filter(
(list): list is string[] => Array.isArray(list),
);
const managed = this.managedModelSettings;
return {
availableModels:
managed.availableModels ?? (lists.length > 0 ? [...new Set(lists.flat())] : undefined),
modelOverrides: {
...this.effective.modelOverrides,
...override?.modelOverrides,
...managed.modelOverrides,
},
};
}
/**
* `idleCompaction`, `autoCompactEnabled` and `env` as the CLI resolves them:
* managed settings outrank the `--settings` override, which outranks the
* user/project/local files. `env` merges entry by entry in the same order. A
* key whose value is `undefined` in the override does not hide the files' value.
*/
getCompactionSettings(settingsOverride?: Settings): CompactionSettings {
const managed = this.managedCompactionSettings;
const env: Record<string, string> = {};
for (const tier of [this.effective.env, settingsOverride?.env, managed.env]) {
for (const [name, value] of Object.entries(tier ?? {})) {
if (value !== undefined) env[name] = value;
}
}
return {
idleCompaction:
managed.idleCompaction ?? settingsOverride?.idleCompaction ?? this.effective.idleCompaction,
autoCompactEnabled:
managed.autoCompactEnabled ??
settingsOverride?.autoCompactEnabled ??
this.effective.autoCompactEnabled,
env,
};
}
/**
* `deniedModels` entries from the managed tier, the only tier the CLI
* honors them from.

View File

@@ -1,7 +1,7 @@
/**
* The `<task-notification>` inputs that Claude Code writes to a transcript
* when a background task stops. The replay of a session (`v1/acp-agent.ts`) and
* the session index (`v1/session-index/background-tasks.ts`) read them here, so
* the session index (`session-index/background-tasks.ts`) read them here, so
* both read the same fields.
*/

View File

@@ -1,4 +1,4 @@
{"kind":"initialize","payload":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","customInstructions","recommendedValue","diffPatch","planFile","backgroundSubagents"],"goal":{"actions":["set","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"logout":{},"status":{}},"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"status":{},"subagents":{},"systemPrompt":{"mode":{"override":{}}}}},"agentInfo":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"authMethods":[],"protocolVersion":1}}
{"kind":"initialize","payload":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","customInstructions","recommendedValue","diffPatch","planFile","backgroundSubagents"],"goal":{"actions":["set","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"logout":{},"status":{}},"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"options":{},"resume":{},"status":{},"subagents":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"agentInfo":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"authMethods":[],"protocolVersion":1}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"content":{"text":"Fix the bug","type":"text"},"messageId":"00000000-0000-4000-8000-00000000c001","sessionUpdate":"user_message_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"content":{"text":"Check the file.","type":"text"},"messageId":"msg_r1","sessionUpdate":"agent_thought_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"content":{"text":"I will edit it.","type":"text"},"messageId":"msg_r1","sessionUpdate":"agent_message_chunk"}}}

View File

@@ -1,3 +1,3 @@
{"kind":"initialize","payload":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","customInstructions","recommendedValue","diffPatch","planFile","backgroundSubagents"],"goal":{"actions":["set","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"logout":{},"status":{}},"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"status":{},"subagents":{},"systemPrompt":{"mode":{"override":{}}}}},"agentInfo":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"authMethods":[],"protocolVersion":1}}
{"kind":"initialize","payload":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","customInstructions","recommendedValue","diffPatch","planFile","backgroundSubagents"],"goal":{"actions":["set","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"logout":{},"status":{}},"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"options":{},"resume":{},"status":{},"subagents":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"agentInfo":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"authMethods":[],"protocolVersion":1}}
{"kind":"newSession","payload":{"configOptions":[{"category":"mode","currentValue":"auto","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"jetbrains":{"air":{"kind":"plan","version":1}}},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","id":"model","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"jetbrains":{"air":{"kind":"plan","version":1}}},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"auto"},"sessionId":"<session>"}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]"},"name":"mcp"}],"sessionUpdate":"available_commands_update"}}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"/goal Ship the feature","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"messageId":"00000000-0000-4000-8000-00000000c001","sessionUpdate":"user_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":{"text":"Fix the bug","type":"text"},"messageId":"00000000-0000-4000-8000-00000000c001","sessionUpdate":"user_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"messageId":"msg_r1","sessionUpdate":"agent_thought"}}}

View File

@@ -1,2 +1,2 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"status":{}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"options":{},"prompt":{"embeddedContext":{},"image":{}},"status":{},"systemPrompt":{"mode":{"override":{}}},"title":{}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"session/new","payload":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]","type":"text"},"name":"mcp","type":"command"}],"configOptions":[{"category":"mode","configId":"mode","currentValue":"auto","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":"<session>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -31,6 +31,13 @@
* - The `initialize` response also advertises
* `agentCapabilities.sessionCapabilities.status`, which origin/main did
* not: the agent now serves `session/status`.
* - The `initialize` response also advertises
* `agentCapabilities.sessionCapabilities.title`, which origin/main did not:
* the agent now serves `session/set_title`.
* - The `initialize` response also advertises
* `agentCapabilities.sessionCapabilities.options`, which origin/main did
* not: the agent now serves `session/options` and takes `configOptions` on
* session setup requests.
* - A SandboxNetworkAccess tool call is settled by a `tool_call_update` with
* only its final `status` once its permission request is answered. The
* callback runs nothing, so origin/main never settled the call (see
@@ -232,19 +239,17 @@ function isMeta(key: string): boolean {
return key.startsWith("_meta.");
}
/** `record` without the session capability `key`, when it is an `initialize` response. */
function withoutSessionCapability(record: Recorded, key: string): Recorded {
/** `record` without the session capabilities `keys`, when it is an `initialize` response. */
function withoutSessionCapabilities(record: Recorded, keys: readonly string[]): Recorded {
if (record.kind !== "initialize") return record;
const payload = record.payload as Json;
const agent = payload.agentCapabilities as Json | undefined;
const session = agent?.sessionCapabilities as Json | undefined;
if (!session || !(key in session)) return record;
if (!session || !keys.some((key) => key in session)) return record;
const remaining = keys.reduce((rest, key) => without(rest, key), session);
return {
...record,
payload: {
...payload,
agentCapabilities: { ...agent, sessionCapabilities: without(session, key) },
},
payload: { ...payload, agentCapabilities: { ...agent, sessionCapabilities: remaining } },
} as Recorded;
}
@@ -267,7 +272,9 @@ function withoutAuthStatusCapability(record: Recorded): Recorded {
*/
export function compareWithBaseline(baseline: Recorded[], traffic: Recorded[]): string[] {
const expected = baseline
.map((record) => withoutSessionCapability(withoutAirOnlyKeys(record) as Recorded, "subagents"))
.map((record) =>
withoutSessionCapabilities(withoutAirOnlyKeys(record) as Recorded, ["subagents"]),
)
.filter((record) => {
const update = updateOf(record);
return !(update?.sessionUpdate === "session_info_update" && Object.keys(update).length === 1);
@@ -275,7 +282,7 @@ export function compareWithBaseline(baseline: Recorded[], traffic: Recorded[]):
const current = withoutSandboxNetworkSettlements(
traffic.map((record) =>
withoutAuthStatusCapability(
withoutSessionCapability(withoutSessionCapability(record, "systemPrompt"), "status"),
withoutSessionCapabilities(record, ["systemPrompt", "status", "title", "options"]),
),
),
);

View File

@@ -9,6 +9,8 @@ import {
type ClientCapabilities,
type InitializeRequest,
} from "@agentclientprotocol/sdk/unstable";
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
/** The context-usage report the base mock query returns. `rawMaxTokens`
* matches the agent's DEFAULT_CONTEXT_WINDOW so window-related assertions
@@ -82,3 +84,73 @@ export function heldRequest<T>(_params?: unknown, signal?: AbortSignal): Promise
else signal?.addEventListener("abort", cancel, { once: true });
});
}
/** A transcript record of a user message. */
export function transcriptUser(uuid: string, content: string, extra: object = {}) {
return { type: "user", uuid, message: { role: "user", content }, ...extra };
}
/** A transcript record of an assistant text response. */
export function transcriptAssistant(uuid: string, text: string) {
return {
type: "assistant",
uuid,
message: {
id: `msg_${uuid}`,
role: "assistant",
model: "claude-opus-5",
content: [{ type: "text", text }],
},
};
}
/** A `compact_boundary` as Claude Code writes it: no parent, and the last
* message before it in `logicalParentUuid`. */
export function transcriptCompactBoundary(
uuid: string,
logicalParentUuid: string,
compactMetadata: object = {},
) {
return {
type: "system",
subtype: "compact_boundary",
uuid,
parentUuid: null,
logicalParentUuid,
compactMetadata: { trigger: "manual", preTokens: 1000, ...compactMetadata },
};
}
/** The summary message that follows a `compact_boundary`. */
export function transcriptCompactSummary(
uuid: string,
content = "This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.\n\n" +
"Summary:\n1. Primary Request and Intent:\n Say hi.\n\n" +
"Continue the conversation from where it left off without asking the user any further questions. Resume directly — do not acknowledge the summary.",
) {
return transcriptUser(uuid, content, { isCompactSummary: true });
}
/** Writes `<directory>/<sessionId>.jsonl` as Claude Code does: each record
* links to the record before it, except a record that sets its own
* `parentUuid`. */
export async function writeTranscript(
directory: string,
sessionId: string,
records: Record<string, unknown>[],
): Promise<void> {
await mkdir(directory, { recursive: true });
let parentUuid: unknown = null;
const lines = records.map((record) => {
const line = {
parentUuid,
sessionId,
isSidechain: false,
timestamp: "2026-10-11T00:00:00.000Z",
...record,
};
parentUuid = record.uuid;
return JSON.stringify(line);
});
await writeFile(path.join(directory, `${sessionId}.jsonl`), lines.join("\n") + "\n");
}

View File

@@ -0,0 +1,92 @@
import { describe, it, expect, afterEach, vi } from "vitest";
import { resolveSettings } from "@anthropic-ai/claude-agent-sdk";
import { SettingsManager } from "../settings.js";
vi.mock("@anthropic-ai/claude-agent-sdk", async (importOriginal) => ({
...(await importOriginal<typeof import("@anthropic-ai/claude-agent-sdk")>()),
resolveSettings: vi.fn(),
}));
describe("model selection settings policy", () => {
let manager: SettingsManager;
afterEach(() => manager?.dispose());
it.each([{ availableModels: ["claude-haiku-4-5"] }, { availableModels: [] }])(
"keeps the managed allowlist %j and mappings above inline settings",
async ({ availableModels }) => {
const managed = {
availableModels,
modelOverrides: { "claude-haiku-4-5": "managed-haiku" },
deniedModels: ["claude-opus-5"],
};
vi.mocked(resolveSettings).mockResolvedValue({
effective: managed,
provenance: {},
sources: [{ source: "managed", settings: managed }],
});
manager = new SettingsManager(process.cwd());
await manager.initialize();
expect(
manager.getModelSelectionSettings({
availableModels: ["claude-opus-5"],
modelOverrides: { "claude-haiku-4-5": "inline-haiku" },
}),
).toEqual({
availableModels: managed.availableModels,
modelOverrides: managed.modelOverrides,
});
expect(manager.getManagedDeniedModels()).toEqual(managed.deniedModels);
},
);
it("keeps managed override keys above inline keys without discarding other inline mappings", async () => {
const managed = { modelOverrides: { "claude-haiku-4-5": "managed-haiku" } };
vi.mocked(resolveSettings).mockResolvedValue({
effective: managed,
provenance: {},
sources: [{ source: "managed", settings: managed }],
});
manager = new SettingsManager(process.cwd());
await manager.initialize();
expect(
manager.getModelSelectionSettings({
availableModels: ["claude-haiku-4-5", "claude-sonnet-4-6"],
modelOverrides: {
"claude-haiku-4-5": "inline-haiku",
"claude-sonnet-4-6": "inline-sonnet",
},
}),
).toEqual({
availableModels: ["claude-haiku-4-5", "claude-sonnet-4-6"],
modelOverrides: { "claude-haiku-4-5": "managed-haiku", "claude-sonnet-4-6": "inline-sonnet" },
});
});
it("preserves file and inline mappings when the managed allowlist supplies no mappings", async () => {
vi.mocked(resolveSettings).mockResolvedValue({
effective: {
availableModels: ["claude-haiku-4-5"],
modelOverrides: { "claude-haiku-4-5": "file-haiku" },
},
provenance: {},
sources: [
{ source: "user", settings: { modelOverrides: { "claude-haiku-4-5": "file-haiku" } } },
{ source: "managed", settings: { availableModels: ["claude-haiku-4-5"] } },
],
});
manager = new SettingsManager(process.cwd());
await manager.initialize();
expect(manager.getModelSelectionSettings()).toEqual({
availableModels: ["claude-haiku-4-5"],
modelOverrides: { "claude-haiku-4-5": "file-haiku" },
});
expect(
manager.getModelSelectionSettings({
modelOverrides: { "claude-haiku-4-5": "inline-haiku" },
}),
).toEqual({
availableModels: ["claude-haiku-4-5"],
modelOverrides: { "claude-haiku-4-5": "inline-haiku" },
});
});
});

View File

@@ -1,9 +1,20 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { SessionMessage } from "@anthropic-ai/claude-agent-sdk";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getSessionMessages, type SessionMessage } from "@anthropic-ai/claude-agent-sdk";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import {
transcriptAssistant,
transcriptCompactBoundary,
transcriptCompactSummary,
transcriptUser,
writeTranscript,
} from "./helpers.js";
import os from "node:os";
import path from "node:path";
import { readResumedTail, resumedModelFromTranscript } from "../resumed-session.js";
import {
readHistoryAcrossCompactions,
readResumedTail,
resumedModelFromTranscript,
} from "../resumed-session.js";
function assistant(
model: unknown,
@@ -203,3 +214,187 @@ describe("readResumedTail", () => {
expect(await readResumedTail("no-mode")).toEqual({ model: "claude-opus-5" });
});
});
describe("readHistoryAcrossCompactions", () => {
const sessionId = "33333333-3333-4333-8333-333333333333";
let configDir: string;
let originalConfigDir: string | undefined;
beforeEach(async () => {
configDir = await mkdtemp(path.join(os.tmpdir(), "claude-acp-compacted-"));
originalConfigDir = process.env.CLAUDE_CONFIG_DIR;
process.env.CLAUDE_CONFIG_DIR = configDir;
});
afterEach(async () => {
if (originalConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR;
else process.env.CLAUDE_CONFIG_DIR = originalConfigDir;
await rm(configDir, { recursive: true, force: true });
});
const transcript = (records: Record<string, unknown>[], project = "-workspace") =>
writeTranscript(path.join(configDir, "projects", project), sessionId, records);
const user = transcriptUser;
const assistant = transcriptAssistant;
const boundary = transcriptCompactBoundary;
const summary = (uuid: string) => transcriptCompactSummary(uuid, `Summary ${uuid}`);
const texts = (messages: SessionMessage[]) =>
messages.map((message) => {
const content = (message.message as { content: unknown }).content;
return typeof content === "string" ? content : (content as { text: string }[])[0]!.text;
});
it("reads the messages before each compaction", async () => {
await transcript([
user("u1", "hi"),
assistant("a1", "Hello"),
boundary("b1", "a1"),
summary("s1"),
user("u2", "next"),
assistant("a2", "Next"),
boundary("b2", "a2"),
summary("s2"),
user("u3", "identify yourself"),
assistant("a3", "Claude"),
]);
// getSessionMessages stops at the last boundary.
const chain = await getSessionMessages(sessionId);
expect(texts(chain)).toEqual(["Summary s2", "identify yourself", "Claude"]);
expect(texts(await readHistoryAcrossCompactions(sessionId, chain))).toEqual([
"hi",
"Hello",
"Summary s1",
"next",
"Next",
"Summary s2",
"identify yourself",
"Claude",
]);
});
it("keeps the messages that a compaction preserved where they were said", async () => {
// Claude Code 2.1.293 keeps u2 and a2 in the context after the summary,
// and records them in both forms of preserved-message metadata.
await transcript([
user("u1", "hi"),
assistant("a1", "Hello"),
user("u2", "keep this"),
assistant("a2", "Kept"),
boundary("b1", "a2", {
preservedSegment: { headUuid: "u2", anchorUuid: "s1", tailUuid: "a2" },
preservedMessages: { anchorUuid: "s1", uuids: ["u2", "a2"] },
}),
summary("s1"),
user("u3", "after"),
assistant("a3", "After"),
]);
// getSessionMessages moves the preserved messages after the summary.
const chain = await getSessionMessages(sessionId);
expect(texts(chain)).toEqual(["Summary s1", "keep this", "Kept", "after", "After"]);
expect(texts(await readHistoryAcrossCompactions(sessionId, chain))).toEqual([
"hi",
"Hello",
"keep this",
"Kept",
"Summary s1",
"after",
"After",
]);
});
it("returns the chain of a session without a compaction as it is", async () => {
await transcript([user("u1", "hi"), assistant("a1", "Hello")]);
const chain = await getSessionMessages(sessionId);
expect(await readHistoryAcrossCompactions(sessionId, chain)).toBe(chain);
});
it("ends at the last message of the chain it was given", async () => {
await transcript([
user("u1", "hi"),
assistant("a1", "Hello"),
boundary("b1", "a1"),
summary("s1"),
user("u2", "identify yourself"),
]);
const chain = await getSessionMessages(sessionId);
// Claude Code appends a response after the first read.
await transcript([
user("u1", "hi"),
assistant("a1", "Hello"),
boundary("b1", "a1"),
summary("s1"),
user("u2", "identify yourself"),
assistant("a2", "Claude"),
]);
expect(texts(await readHistoryAcrossCompactions(sessionId, chain))).toEqual([
"hi",
"Hello",
"Summary s1",
"identify yourself",
]);
});
it.each([
["the transcript is gone", async () => rm(configDir, { recursive: true, force: true })],
[
"the transcript no longer has the last message",
() =>
transcript([
user("u1", "hi"),
assistant("a1", "Hello"),
boundary("b1", "a1"),
summary("s1"),
user("u9", "other"),
]),
],
])("returns the chain and logs when %s", async (_case, change) => {
await transcript([
user("u1", "hi"),
assistant("a1", "Hello"),
boundary("b1", "a1"),
summary("s1"),
user("u2", "identify yourself"),
]);
const chain = await getSessionMessages(sessionId);
await change();
const error = vi.fn();
expect(await readHistoryAcrossCompactions(sessionId, chain, { log: () => {}, error })).toBe(
chain,
);
expect(error).toHaveBeenCalledOnce();
});
it("reads the transcript in the project directory it is given", async () => {
await transcript(
[
user("u1", "elsewhere"),
assistant("a1", "Elsewhere"),
boundary("b1", "a1"),
summary("s1"),
user("u2", "identify yourself"),
],
"-a-other",
);
await transcript(
[
user("u1", "hi"),
assistant("a1", "Hello"),
boundary("b1", "a1"),
summary("s1"),
user("u2", "identify yourself"),
],
"-z-workspace",
);
const chain = await getSessionMessages(sessionId, { dir: "/z/workspace" });
expect(
texts(await readHistoryAcrossCompactions(sessionId, chain, undefined, "/z/workspace")),
).toEqual(["hi", "Hello", "Summary s1", "identify yourself"]);
});
});

View File

@@ -0,0 +1,50 @@
import { describe, expect, it } from "vitest";
import { apiRetryWarning } from "../session-failure-extension.js";
describe("apiRetryWarning", () => {
// Each input is one the bundled CLI sends: the CLI derives `error` from the
// status (`IIt`), and an error with no HTTP response has a null status.
it.each([
{
name: "a no_response retry, which the CLI sends with a 0 ms delay",
message: {
error_status: null,
error: "unknown" as const,
retry_delay_ms: 0,
no_response: { waited_ms: 180_000, retry_wait_ms: 600_000 },
},
details: "Connection error: no HTTP response. Next attempt now.",
},
{
name: "a background rate-limit wait in minutes, rounded up",
message: { error_status: 429, error: "rate_limit" as const, retry_delay_ms: 1_740_500 },
details: "HTTP 429 (rate limited). Next attempt in 30m.",
},
{
name: "a wait just under 120 s in seconds",
message: { error_status: 529, error: "overloaded" as const, retry_delay_ms: 119_000 },
details: "HTTP 529 (overloaded). Next attempt in 119s.",
},
{
name: "a wait of 120 s in minutes",
message: { error_status: 529, error: "overloaded" as const, retry_delay_ms: 120_000 },
details: "HTTP 529 (overloaded). Next attempt in 2m.",
},
{
name: "a credential error, which has no HTTP status",
message: {
error_status: null,
error: "cloud_credential_error" as const,
retry_delay_ms: 1_100,
},
details: "No HTTP status (cloud credential error). Next attempt in 1s.",
},
{
name: "a message with no error kind as unknown",
message: { error_status: 503, error: undefined, retry_delay_ms: 600 },
details: "HTTP 503. Next attempt in 1s.",
},
])("describes $name", ({ message, details }) => {
expect(apiRetryWarning({ attempt: 1, max_retries: 10, ...message }).details).toBe(details);
});
});

View File

@@ -2,28 +2,28 @@ import { describe, expect, it } from "vitest";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { deriveActivity, selectCost, type ActivityState } from "../../v1/session-index/activity.js";
import { LiveSessionRegistry, type LiveRecord } from "../../v1/session-index/live-registry.js";
import { isSessionId } from "../../paths.js";
import { deriveActivity, selectCost, type ActivityState } from "../session-index/activity.js";
import { LiveSessionRegistry, type LiveRecord } from "../session-index/live-registry.js";
import { isSessionId } from "../paths.js";
import {
encodeProjectPath,
pathAndAncestors,
projectDirMatches,
} from "../../v1/session-index/project-dirs.js";
} from "../session-index/project-dirs.js";
import {
decodeListCursor,
encodeListCursor,
parseListOptions,
parseRenameSessionRequest,
type ListScope,
} from "../../v1/session-index/service.js";
} from "../session-index/service.js";
import {
effectiveTitle,
isArchivedTitle,
storedTitle,
titleRecords,
visibleTitle,
} from "../../v1/session-index/archive-title.js";
} from "../session-index/archive-title.js";
import {
agentWorktreeOf,
continuedInSessionId,
@@ -33,11 +33,11 @@ import {
sdkTitles,
transcriptAgentName,
type TranscriptFacts,
} from "../../v1/session-index/transcript-scan.js";
import { firstPrompt } from "../../v1/session-index/first-prompt.js";
import { DirListings, statFiles } from "../../v1/session-index/dir-listing.js";
import { SessionIndex, type IndexRow } from "../../v1/session-index/session-index.js";
import { changeSignature, sessionInfoOf } from "../../v1/session-index/session-info.js";
} from "../session-index/transcript-scan.js";
import { firstPrompt } from "../session-index/first-prompt.js";
import { DirListings, statFiles } from "../session-index/dir-listing.js";
import { SessionIndex, type IndexRow } from "../session-index/session-index.js";
import { changeSignature, sessionInfoOf } from "../session-index/session-info.js";
const SESSION = "11111111-1111-4111-8111-111111111111";
const lines = (...entries: object[]) => entries.map((entry) => JSON.stringify(entry)).join("\n");

View File

@@ -38,3 +38,62 @@ describe("SettingsManager.getApiKeyHelper", () => {
manager.dispose();
});
});
describe("SettingsManager.getCompactionSettings", () => {
it("ranks managed settings over the --settings override over the files, env entry by entry", async () => {
const manager = await managerResolving({
effective: {
idleCompaction: true,
autoCompactEnabled: false,
env: {
DISABLE_COMPACT: "1",
DISABLE_AUTO_COMPACT: "1",
CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS: "1",
},
},
provenance: {},
sources: [
{ source: "userSettings", settings: { autoCompactEnabled: false } },
{
source: "managed",
settings: { idleCompaction: true, env: { DISABLE_AUTO_COMPACT: "1" } },
},
],
});
expect(
manager.getCompactionSettings({
idleCompaction: false,
autoCompactEnabled: true,
env: { DISABLE_COMPACT: "0", DISABLE_AUTO_COMPACT: "0" },
}),
).toEqual({
idleCompaction: true,
autoCompactEnabled: true,
env: {
DISABLE_COMPACT: "0",
DISABLE_AUTO_COMPACT: "1",
CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS: "1",
},
});
manager.dispose();
});
it("keeps the files' value for a key the override sets to undefined", async () => {
const manager = await managerResolving({
effective: { idleCompaction: false, env: { DISABLE_COMPACT: "1" } },
provenance: {},
sources: [],
});
expect(
manager.getCompactionSettings({
idleCompaction: undefined,
env: { DISABLE_COMPACT: undefined as unknown as string },
}),
).toEqual({
idleCompaction: false,
autoCompactEnabled: undefined,
env: { DISABLE_COMPACT: "1" },
});
manager.dispose();
});
});

View File

@@ -921,6 +921,136 @@ describe("ClaudeAcpAgent settings", () => {
return { setModelSpy };
}
it.each(["environment", "caller object", "caller file"])(
"includes configured models omitted by the SDK from %s settings",
async (source) => {
const configured = {
availableModels: ["claude-haiku-4-5", "claude-haiku-5-5"],
modelOverrides: {
"claude-haiku-4-5": "global.anthropic.claude-haiku-4-5-20251001-v1:0",
"claude-haiku-5-5": "global.anthropic.claude-haiku-5-5",
},
};
let settings: object | string | undefined;
if (source === "environment") {
vi.stubEnv("CLAUDE_MODEL_CONFIG", JSON.stringify(configured));
} else if (source === "caller file") {
settings = "caller-settings.json";
await fs.promises.writeFile(path.join(tempDir, settings), JSON.stringify(configured));
} else {
settings = configured;
}
const { setModelSpy } = mockQueryWithModels([
{ value: "default", displayName: "Default", description: "" },
{ value: "sonnet", displayName: "Sonnet", description: "" },
]);
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent = new ClaudeAcpAgent(createMockClient());
try {
const response = await (agent as any).createSession({
cwd: tempDir,
mcpServers: [],
_meta: { claudeCode: { options: { settings } } },
});
expect(querySpy.mock.calls[0][0].options.settings).toEqual(configured);
const ids = ["default", ...Object.values(configured.modelOverrides)];
expect(
agent.sessions[response.sessionId].models.availableModels.map(
(model: any) => model.modelId,
),
).toEqual(ids);
const modelOption = response.configOptions.find((option: any) => option.id === "model");
expect(modelOption.options.map((option: any) => option.value)).toEqual(ids);
await agent.setSessionConfigOption({
sessionId: response.sessionId,
configId: "model",
value: ids[2],
});
expect(setModelSpy).toHaveBeenLastCalledWith(ids[2]);
expect(agent.sessions[response.sessionId].modelInfos.map((model) => model.value)).toEqual(
ids,
);
} finally {
await agent.dispose();
vi.unstubAllEnvs();
}
},
);
it("merges file settings with caller settings after caller keys replace the env config", async () => {
await fs.promises.writeFile(
path.join(tempDir, "settings.json"),
JSON.stringify({
availableModels: ["claude-sonnet-4-6", "claude-haiku-4-5"],
modelOverrides: {
"claude-sonnet-4-6": "file-sonnet",
"claude-haiku-4-5": "file-haiku",
},
}),
);
vi.stubEnv(
"CLAUDE_MODEL_CONFIG",
JSON.stringify({
availableModels: ["claude-opus-4-6"],
modelOverrides: { "claude-sonnet-4-6": "env-sonnet", "claude-haiku-4-5": "env-haiku" },
}),
);
mockQueryWithModels([{ value: "default", displayName: "Default", description: "" }]);
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent = new ClaudeAcpAgent(createMockClient());
const settings = {
availableModels: ["claude-haiku-4-5", "claude-haiku-5-5"],
modelOverrides: { "claude-haiku-4-5": "caller-haiku" },
};
try {
const response = await (agent as any).createSession({
cwd: tempDir,
mcpServers: [],
_meta: { claudeCode: { options: { settings } } },
});
expect(querySpy.mock.calls[0][0].options.settings).toEqual(settings);
const modelOption = response.configOptions.find((option: any) => option.id === "model");
expect(modelOption.options.map((option: any) => option.value)).toEqual([
"default",
"file-sonnet",
"caller-haiku",
"claude-haiku-5-5",
]);
} finally {
await agent.dispose();
vi.unstubAllEnvs();
}
});
it.each([[], undefined])(
"preserves empty and absent configured allowlists: %j",
async (availableModels) => {
vi.stubEnv(
"CLAUDE_MODEL_CONFIG",
JSON.stringify({
availableModels,
modelOverrides: { "claude-haiku-4-5": "provider-haiku" },
}),
);
mockQueryWithModels([
{ value: "default", displayName: "Default", description: "" },
{ value: "sonnet", displayName: "Sonnet", description: "" },
]);
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const agent = new ClaudeAcpAgent(createMockClient());
try {
const response = await (agent as any).createSession({ cwd: tempDir, mcpServers: [] });
const modelOption = response.configOptions.find((option: any) => option.id === "model");
expect(modelOption.options.map((option: any) => option.value)).toEqual(
availableModels ? ["default"] : ["default", "sonnet"],
);
} finally {
await agent.dispose();
vi.unstubAllEnvs();
}
},
);
it("restricts configOptions to the user's allowlist using their exact IDs", async () => {
// Reproduces the scenario from
// https://github.com/agentclientprotocol/claude-agent-acp/issues/620:

View File

@@ -28,12 +28,9 @@ import { nodeToWebWritable, nodeToWebReadable } from "../../utils.js";
import { createPostToolUseHook, markdownEscape, toDisplayPath } from "../../tools.js";
import { toolUpdateFromDiffToolResponse } from "../../diff.js";
import { ToolCallFieldTracker } from "../../tool-calls/field-tracker.js";
import {
ClaudeAcpAgent,
computeSessionFingerprint,
runPromptWithCancellation,
type SteerRequest,
} from "../../v1/acp-agent.js";
import { ClaudeAcpAgent, runPromptWithCancellation } from "../../v1/acp-agent.js";
import { computeSessionFingerprint } from "../../v1/session-fingerprint.js";
import type { SteerRequest } from "../../v1/steering.js";
import { buildConfigOptions, discoverCustomAgents } from "../../session-config-options.js";
import { createFastModeConfigOption } from "../../session-fast-mode.js";
import {
@@ -58,7 +55,16 @@ import {
parseUsageResponse,
} from "../../usage-markdown.js";
import { Pushable } from "../../utils.js";
import { heldRequest, initializeClient, makeMockQuery } from "../helpers.js";
import {
heldRequest,
initializeClient,
makeMockQuery,
transcriptAssistant,
transcriptCompactBoundary,
transcriptCompactSummary,
transcriptUser,
writeTranscript,
} from "../helpers.js";
import { acpProtocolRouter } from "../../serve.js";
import * as agentSdk from "@anthropic-ai/claude-agent-sdk";
import {
@@ -73,6 +79,7 @@ import {
PermissionUpdate,
query,
SDKAssistantMessage,
type SDKAssistantMessageError,
type Options,
type SDKControlGetUsageResponse,
} from "@anthropic-ai/claude-agent-sdk";
@@ -3629,6 +3636,63 @@ describe("usage-limit failure replay", () => {
});
});
describe("compacted session replay", () => {
it("replays the messages before the compaction, then the compaction", async () => {
const configDir = await mkdtemp(path.join(os.tmpdir(), "claude-acp-compacted-replay-"));
const originalConfigDir = process.env.CLAUDE_CONFIG_DIR;
process.env.CLAUDE_CONFIG_DIR = configDir;
try {
const sessionId = "44444444-4444-4444-8444-444444444444";
// The records of `/compact` between two prompts, as Claude Code writes
// them: the boundary has no parent and links "Hello" logically.
await writeTranscript(path.join(configDir, "projects", "-tmp-proj"), sessionId, [
transcriptUser("u1", "hi"),
transcriptAssistant("a1", "Hello"),
transcriptCompactBoundary("b1", "a1"),
transcriptCompactSummary("s1"),
transcriptUser("u2", "identify yourself"),
transcriptAssistant("a2", "Claude"),
]);
const updates: SessionNotification[] = [];
const client = {
sessionUpdate: async (update: SessionNotification) => {
updates.push(update);
},
} as unknown as AcpClient;
const agent = new ClaudeAcpAgent(client, { log: () => {}, error: () => {} });
await initializeClient(agent, { session: { compaction: {} } } as ClientCapabilities);
await (
agent as unknown as { replaySessionHistory(sessionId: string): Promise<void> }
).replaySessionHistory(sessionId);
expect(
updates.flatMap(({ update }) => {
if (update.sessionUpdate === "compaction_update") return [["compaction", update.status]];
if (
(update.sessionUpdate === "user_message_chunk" ||
update.sessionUpdate === "agent_message_chunk") &&
update.content.type === "text"
) {
return [[update.sessionUpdate, update.content.text]];
}
return [];
}),
).toEqual([
["user_message_chunk", "hi"],
["agent_message_chunk", "Hello"],
["compaction", "completed"],
["user_message_chunk", "identify yourself"],
["agent_message_chunk", "Claude"],
]);
} finally {
if (originalConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR;
else process.env.CLAUDE_CONFIG_DIR = originalConfigDir;
await rm(configDir, { recursive: true, force: true });
}
});
});
describe("session/list", () => {
const sdkSession = (index: number) => ({
sessionId: `session-${index}`,
@@ -11834,7 +11898,7 @@ describe("stop reason propagation", () => {
subtype: "api_retry",
attempt: 1,
max_retries: 5,
retry_delay_ms: 100,
retry_delay_ms: 600,
error_status: 429,
error: "rate_limit",
uuid: randomUUID(),
@@ -11864,6 +11928,7 @@ describe("stop reason propagation", () => {
severity: "warning",
revision: 1,
title: "Retrying Claude, attempt 1 of 5.",
details: "HTTP 429 (rate limited). Next attempt in 1s.",
actions: [],
}),
);
@@ -11877,6 +11942,144 @@ describe("stop reason propagation", () => {
actions: ["retry"],
}),
);
expect(terminal).not.toHaveProperty("details");
});
it("keeps the api retries of one turn and its failure in one row before the echo", async () => {
const updates: SessionNotification[] = [];
const agent = new ClaudeAcpAgent(
{
sessionUpdate: async (update: SessionNotification) => updates.push(update),
} as unknown as AcpClient,
{ log: () => {}, error: () => {} },
);
await initializeClient(agent, airSessionFailureCapabilities);
const retry = (attempt: number) => ({
type: "system" as const,
subtype: "api_retry" as const,
attempt,
max_retries: 10,
retry_delay_ms: 1_000,
error_status: 503,
error: "server_error" as const,
uuid: randomUUID(),
session_id: "test-session",
});
const input = new Pushable<any>();
async function* cli() {
const iter = input[Symbol.asyncIterator]();
for (const turn of [1, 2]) {
const { value: userMessage } = await iter.next();
const echo = {
type: "user",
message: userMessage.message,
parent_tool_use_id: null,
uuid: userMessage.uuid,
session_id: "test-session",
isReplay: true,
};
// The CLI holds the echo back until the first assistant frame or
// stream event of the turn. A request that gets a 503 has neither, so
// its retries come before the echo.
if (turn === 1) {
yield retry(1);
yield retry(2);
yield echo;
yield createAssistantError("server_error", "API Error: 503");
yield createResultMessage({
subtype: "success",
stop_reason: "end_turn",
is_error: true,
result: "API Error: 503",
});
} else {
yield retry(1);
yield echo;
yield createResultMessage({
subtype: "success",
stop_reason: "end_turn",
is_error: false,
result: "ok",
});
}
}
}
agent.sessions["test-session"] = mockSessionState({ query: wrapQuery(cli()), input });
const first = await agent.prompt({
sessionId: "test-session",
prompt: [{ type: "text", text: "first" }],
});
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "second" }] });
const warnings = updates
.map((update) => (update.update._meta as any)?.jetbrains?.air?.sessionFailure)
.filter((failure) => failure?.severity === "warning");
expect(warnings.map((warning) => warning.revision)).toEqual([1, 2, 1]);
expect(warnings[1].id).toBe(warnings[0].id);
expect(warnings[0].id).toMatch(/:error$/);
expect(sessionFailureFromResponse(first)).toEqual(
expect.objectContaining({ id: warnings[0].id, revision: 3, severity: "error" }),
);
expect(warnings[2].id).not.toBe(warnings[0].id);
});
it("replaces the cause of an api retry warning on the next retry", async () => {
const updates: SessionNotification[] = [];
const agent = new ClaudeAcpAgent(
{
sessionUpdate: async (update: SessionNotification) => updates.push(update),
} as unknown as AcpClient,
{ log: () => {}, error: () => {} },
);
await initializeClient(agent, airSessionFailureCapabilities);
const retry = (
attempt: number,
error_status: number | null,
error: SDKAssistantMessageError,
retry_delay_ms: number,
) => ({
type: "system" as const,
subtype: "api_retry" as const,
attempt,
max_retries: 10,
retry_delay_ms,
error_status,
error,
uuid: randomUUID(),
session_id: "test-session",
});
injectSession(agent, [
retry(1, 529, "overloaded", 8_000),
// An SSE `overloaded_error` event after HTTP 200: the CLI reports
// `overloaded` with no status.
retry(2, null, "overloaded", 1_100),
createResultMessage({
subtype: "success",
stop_reason: "end_turn",
is_error: false,
result: "ok",
}),
]);
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "test" }] });
const warnings = updates
.map((update) => (update.update._meta as any)?.jetbrains?.air?.sessionFailure)
.filter(Boolean);
expect(warnings).toEqual([
expect.objectContaining({
revision: 1,
title: "Retrying Claude, attempt 1 of 10.",
details: "HTTP 529 (overloaded). Next attempt in 8s.",
}),
expect.objectContaining({
id: warnings[0].id,
revision: 2,
title: "Reconnecting to Claude, attempt 2 of 10.",
details: "No HTTP status (overloaded). Next attempt in 1s.",
}),
]);
});
it("says how long a no-response retry waited and will wait", async () => {
@@ -11940,7 +12143,7 @@ describe("stop reason propagation", () => {
subtype: "api_retry",
attempt: 2,
max_retries: 5,
retry_delay_ms: 100,
retry_delay_ms: 1_100,
error_status: null,
error: "unknown",
uuid: randomUUID(),
@@ -12522,7 +12725,7 @@ describe("stop reason propagation", () => {
subtype: "api_retry",
attempt: 1,
max_retries: 10,
retry_delay_ms: 100,
retry_delay_ms: 600,
error_status: 401,
error: "authentication_failed",
uuid: randomUUID(),

View File

@@ -220,7 +220,8 @@ describe("auth status mappers", () => {
state: "authenticated",
accounts: [
{
label: "Anthropic API key (env)",
label: "Anthropic API key",
description: "env",
active: true,
_meta: { claudeCode: { kind: "api_key", detail: "env" } },
},
@@ -842,7 +843,8 @@ describe("auth status over ACP", () => {
state: "authenticated",
accounts: [
{
label: "Custom model gateway (gw.example.com)",
label: "Custom model gateway",
description: "gw.example.com",
active: true,
_meta: { claudeCode: { kind: "gateway", detail: "gw.example.com" } },
},
@@ -860,7 +862,7 @@ describe("auth status over ACP", () => {
agent.setAuthStatus({ kind: "api_key", label: "Anthropic API key", detail: "env" });
expect(await agent.authStatusResponse()).toMatchObject({
state: "authenticated",
accounts: [{ label: "Anthropic API key (env)" }],
accounts: [{ label: "Anthropic API key", description: "env" }],
});
});

View File

@@ -11,11 +11,11 @@ import * as fsSync from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import type { SessionInfo } from "@agentclientprotocol/sdk/unstable";
import type { ActivityState } from "../../v1/session-index/activity.js";
import { endedTaskId, searchBackgroundTasks } from "../../v1/session-index/background-tasks.js";
import { encodeProjectPath } from "../../v1/session-index/project-dirs.js";
import { SessionIndex } from "../../v1/session-index/session-index.js";
import { SessionIndexService } from "../../v1/session-index/service.js";
import type { ActivityState } from "../../session-index/activity.js";
import { endedTaskId, searchBackgroundTasks } from "../../session-index/background-tasks.js";
import { encodeProjectPath } from "../../session-index/project-dirs.js";
import { SessionIndex } from "../../session-index/session-index.js";
import { SessionIndexService } from "../../session-index/service.js";
let configDir: string;
let workspace: string;

View File

@@ -1365,6 +1365,229 @@ describe("createSession options merging", () => {
});
});
describe("session options", () => {
const cwd = process.cwd();
const TWO_MODELS = [
{
value: "claude-sonnet-4-6",
displayName: "Claude Sonnet",
description: "Fast",
supportsAutoMode: true,
supportsEffort: true,
supportedEffortLevels: ["low", "medium", "high"],
},
{
value: "claude-haiku-4-5",
displayName: "Claude Haiku",
description: "Faster",
supportsEffort: true,
supportedEffortLevels: ["low", "medium"],
},
];
let updates: SessionNotification[];
const values = (response: { configOptions?: { id: string; currentValue: unknown }[] | null }) =>
Object.fromEntries((response.configOptions ?? []).map((o) => [o.id, o.currentValue]));
beforeEach(() => {
initModels = TWO_MODELS;
updates = [];
vi.spyOn(agent.client, "sessionUpdate").mockImplementation(async (notification) => {
updates.push(notification as SessionNotification);
});
});
it("advertises session/options", async () => {
const response = await agent.initialize({ protocolVersion: 1, clientCapabilities: {} });
expect(response.agentCapabilities?.sessionCapabilities?.options).toEqual({});
});
it("answers session/options with what session/new returns, from a query that it closes", async () => {
const options = await agent.sessionOptions({ cwd, additionalDirectories: ["/extra"] });
expect(capturedOptions).toMatchObject({
cwd,
additionalDirectories: ["/extra"],
mcpServers: {},
persistSession: false,
});
expect(capturedOptions!.resume).toBeUndefined();
expect(closeSpy).toHaveBeenCalledTimes(1);
expect(agent.sessions).toEqual({});
expect(updates).toEqual([]);
const { sessionId, ...created } = await agent.newSession({ cwd, mcpServers: [] });
expect(sessionId).toBeDefined();
expect(capturedOptions!.persistSession).toBeUndefined();
expect(options).toEqual(created);
});
it("answers session/options of a held session with its current options, without a query", async () => {
const { sessionId } = await agent.newSession({ cwd, mcpServers: [] });
const { configOptions } = await agent.setSessionConfigOption({
sessionId,
configId: "model",
value: "claude-haiku-4-5",
});
capturedOptions = undefined;
const options = await agent.sessionOptions({ cwd, sessionId });
expect(capturedOptions).toBeUndefined();
expect(options).toEqual({ modes: agent.sessions[sessionId]!.modes, configOptions });
expect(values(options).model).toBe("claude-haiku-4-5");
});
it("answers session/options of a stored session from a query that resumes it", async () => {
const sessionId = "7f1c5b0e-3c53-4c0c-9d0a-0d6c1f0a2b11";
await agent.sessionOptions({ cwd, sessionId });
expect(capturedOptions).toMatchObject({ resume: sessionId, persistSession: false });
expect(capturedOptions!.sessionId).toBeUndefined();
expect(closeSpy).toHaveBeenCalledTimes(1);
expect(agent.sessions).toEqual({});
});
it("fails session/options of an unknown session with -32002", async () => {
const sessionId = "7f1c5b0e-3c53-4c0c-9d0a-0d6c1f0a2b11";
initError = new Error(`No conversation found with session ID: ${sessionId}`);
await expect(agent.sessionOptions({ cwd, sessionId })).rejects.toMatchObject({
code: RequestError.sessionNotFound(sessionId).code,
data: { sessionId },
});
expect(closeSpy).toHaveBeenCalledTimes(1);
});
it("starts a new session with the request's configOptions, in list order, without an update", async () => {
const setModels: string[] = [];
setModelImpl = async (model) => void setModels.push(model);
const response = await agent.newSession({
cwd,
mcpServers: [],
configOptions: [
{ configId: "model", value: "claude-haiku-4-5" },
// Haiku offers no "high": the entry is checked against the options the model left.
{ configId: "effort", value: "high" },
{ configId: "mode", value: "plan" },
],
});
expect(setModels).toEqual(["claude-haiku-4-5"]);
expect(values(response)).toMatchObject({ model: "claude-haiku-4-5", mode: "plan" });
expect(values(response).effort).not.toBe("high");
expect(response.modes?.currentModeId).toBe("plan");
expect(response.configOptions).toEqual(agent.sessions[response.sessionId]!.configOptions);
expect(updates.filter((u) => u.sessionId === response.sessionId)).toEqual([]);
// A restart of the query restores the session's choices, not the request's.
expect(agent.sessions[response.sessionId]!.creationParams).not.toHaveProperty(
"configOptions",
);
});
it.each([
["an unknown configId", { configId: "nope", value: "x" }],
["a boolean for a select", { configId: "model", type: "boolean" as const, value: true }],
["a value the option does not offer", { configId: "model", value: "gpt-4" }],
])("skips an entry with %s and sets the entries after it", async (_name, entry) => {
const plain = await agent.newSession({ cwd, mcpServers: [] });
const response = await agent.newSession({
cwd,
mcpServers: [],
configOptions: [entry, { configId: "mode", value: "plan" }],
});
expect(values(response)).toEqual({ ...values(plain), mode: "plan" });
});
it("skips an entry that Claude Code refuses", async () => {
setModelImpl = async () => {
throw new Error("model refused");
};
const errors = vi.spyOn(console, "error").mockImplementation(() => {});
const response = await agent.newSession({
cwd,
mcpServers: [],
configOptions: [
{ configId: "model", value: "claude-haiku-4-5" },
{ configId: "mode", value: "plan" },
],
});
expect(values(response)).toMatchObject({ model: "claude-sonnet-4-6", mode: "plan" });
expect(errors).toHaveBeenCalledWith(
"Could not set the initial value of config option model:",
expect.any(Error),
);
errors.mockRestore();
});
it.each(["newSession", "loadSession", "resumeSession"] as const)(
"%s rejects two entries of one configId before it starts a query",
async (method) => {
await expect(
agent[method]({
sessionId: "7f1c5b0e-3c53-4c0c-9d0a-0d6c1f0a2b11",
cwd,
mcpServers: [],
configOptions: [
{ configId: "mode", value: "plan" },
{ configId: "mode", value: "default" },
],
}),
).rejects.toMatchObject({
code: RequestError.invalidParams().code,
data: { configId: "mode" },
});
expect(capturedOptions).toBeUndefined();
},
);
it.each(["loadSession", "resumeSession"] as const)(
"%s sets the request's configOptions on the session, a held one too",
async (method) => {
const sessionId = "7f1c5b0e-3c53-4c0c-9d0a-0d6c1f0a2b11";
const started = await agent[method]({
sessionId,
cwd,
mcpServers: [],
configOptions: [{ configId: "mode", value: "plan" }],
});
expect(values(started).mode).toBe("plan");
expect(started.modes?.currentModeId).toBe("plan");
const held = await agent[method]({
sessionId,
cwd,
mcpServers: [],
configOptions: [{ configId: "model", value: "claude-haiku-4-5" }],
});
expect(values(held)).toMatchObject({ mode: "plan", model: "claude-haiku-4-5" });
expect(updates.map((u) => u.update.sessionUpdate)).not.toContain("current_mode_update");
expect(updates.map((u) => u.update.sessionUpdate)).not.toContain("config_option_update");
},
);
it("leaves the warning of an Auto mode fallback for the first prompt", async () => {
const response = await agent.newSession({
cwd,
mcpServers: [],
configOptions: [
{ configId: "mode", value: "auto" },
// Haiku cannot serve Auto.
{ configId: "model", value: "claude-haiku-4-5" },
],
});
expect(values(response)).toMatchObject({ mode: "acceptEdits", model: "claude-haiku-4-5" });
expect(updates.filter((u) => u.sessionId === response.sessionId)).toEqual([]);
expect(agent.sessions[response.sessionId]!.autoModeFallbackWarningPending).toBe(true);
});
});
describe("cwd validation", () => {
it("rejects a relative cwd with invalidParams", async () => {
await expect(

View File

@@ -16,6 +16,14 @@ import {
FAST_MODE_OFF,
} from "../../session-fast-mode.js";
import type { AcpClient } from "../../acp-client.js";
import { Pushable } from "../../utils.js";
import { initializeClient } from "../helpers.js";
import {
mockSessionState,
successfulResultMessage,
userEcho,
wrapQuery,
} from "./session-doubles.js";
const MODES = {
currentModeId: "default",
@@ -33,6 +41,8 @@ const MODEL_INFOS: ModelInfo[] = [
{ value: "claude-opus-4-8", displayName: "Claude Opus", description: "Most capable" },
];
const FUTURE_REASON = "some_future_reason" as FastModeDisabledReason;
describe("createFastModeConfigOption", () => {
it("produces a native boolean toggle when the client opted in", () => {
expect(createFastModeConfigOption(true, true)).toEqual({
@@ -45,20 +55,40 @@ describe("createFastModeConfigOption", () => {
});
});
it("explains a blocking disabled reason while the toggle reads off", () => {
const option = createFastModeConfigOption(false, true, "free");
expect(option.description).toBe(
"Faster responses on supported models — not available on the free plan",
);
});
/**
* Scenario:
* 1. Build the option with the toggle off and a disabled reason.
* Expected:
* - The description is the base text plus the reason's explanation.
* - A code the table lacks gets the generic text naming the code.
*/
it.each([
["free", "not available on the free plan"],
["preference", "not enabled for your organization or API gateway"],
["unknown", "currently unavailable"],
[FUTURE_REASON, "not available (some_future_reason)"],
] as [FastModeDisabledReason, string][])(
"explains the disabled reason %s while the toggle reads off",
(reason, explanation) => {
const option = createFastModeConfigOption(false, true, reason);
expect(option.description).toBe(`Faster responses on supported models — ${explanation}`);
},
);
it("ignores routine and on-state reasons", () => {
/**
* Scenario:
* 1. Build the option, toggle off, with `sdk_opt_in_required` or `pending`.
* 2. Build the option, toggle on, with `free`.
* Expected:
* - The description stays the base text in all three cases.
*/
it("ignores unexplained and on-state reasons", () => {
// Every SDK session starts at `sdk_opt_in_required` — the toggle IS the
// opt-in, so it must not read as a blocker.
expect(createFastModeConfigOption(false, true, "sdk_opt_in_required").description).toBe(
"Faster responses on supported models",
);
expect(createFastModeConfigOption(false, true, "preference").description).toBe(
expect(createFastModeConfigOption(false, true, "pending").description).toBe(
"Faster responses on supported models",
);
// A reason riding an `on` state isn't blocking anything right now.
@@ -133,25 +163,38 @@ describe("fastModeStateEnabled", () => {
});
describe("normalizeFastModeDisabledReason", () => {
it("retains only reasons we have an explanation for", () => {
expect(normalizeFastModeDisabledReason("free")).toBe("free");
expect(normalizeFastModeDisabledReason("extra_usage_disabled")).toBe("extra_usage_disabled");
expect(normalizeFastModeDisabledReason("model_not_allowed")).toBe("model_not_allowed");
expect(normalizeFastModeDisabledReason("not_first_party")).toBe("not_first_party");
expect(normalizeFastModeDisabledReason("disabled_by_env")).toBe("disabled_by_env");
expect(normalizeFastModeDisabledReason("network_error")).toBe("network_error");
/**
* Scenario:
* 1. Normalize each explained reason, including `preference`, `unknown` and a future code.
* Expected:
* - Each reason is returned unchanged.
*/
it.each([
"free",
"extra_usage_disabled",
"model_not_allowed",
"not_first_party",
"disabled_by_env",
"network_error",
"preference",
"unknown",
FUTURE_REASON,
] as FastModeDisabledReason[])("retains the explained reason %s", (reason) => {
expect(normalizeFastModeDisabledReason(reason)).toBe(reason);
});
it("drops routine states and unknown values", () => {
expect(normalizeFastModeDisabledReason("sdk_opt_in_required")).toBeUndefined();
expect(normalizeFastModeDisabledReason("preference")).toBeUndefined();
expect(normalizeFastModeDisabledReason("pending")).toBeUndefined();
expect(normalizeFastModeDisabledReason("unknown")).toBeUndefined();
expect(normalizeFastModeDisabledReason(undefined)).toBeUndefined();
expect(
normalizeFastModeDisabledReason("some_future_reason" as FastModeDisabledReason),
).toBeUndefined();
});
/**
* Scenario:
* 1. Normalize `sdk_opt_in_required`, `pending` and `undefined`.
* Expected:
* - Each normalizes to `undefined`.
*/
it.each(["sdk_opt_in_required", "pending", undefined] as (FastModeDisabledReason | undefined)[])(
"drops the unexplained reason %s",
(reason) => {
expect(normalizeFastModeDisabledReason(reason)).toBeUndefined();
},
);
});
describe("buildConfigOptions Fast mode", () => {
@@ -403,29 +446,61 @@ describe("syncFastModeState (SDK-driven state changes)", () => {
expect(sessionUpdates).toHaveLength(0);
});
it("explains a blocking reason once when the SDK refuses a toggle the user turned on", async () => {
/**
* Scenario:
* 1. The toggle is on and the SDK reports `off` with a reason.
* 2. The SDK reports the same `off` and reason again.
* Expected:
* - The toggle is cleared and the reason retained.
* - One transcript line carries the explanation, then one `config_option_update`.
* - The repeat report sends nothing.
*/
it.each([
["extra_usage_disabled", "requires extra usage to be enabled for this account"],
["preference", "not enabled for your organization or API gateway"],
["unknown", "currently unavailable"],
[FUTURE_REASON, "not available (some_future_reason)"],
] as [FastModeDisabledReason, string][])(
"explains the blocking reason %s once when the SDK refuses a toggle the user turned on",
async (reason, explanation) => {
const { sync, session, sessionUpdates } = setup({ fastModeEnabled: true, withOption: true });
await sync(SESSION_ID, session, "off", reason);
expect(session.fastModeEnabled).toBe(false);
expect(session.fastModeDisabledReason).toBe(reason);
expect(sessionUpdates).toHaveLength(2);
expect(sessionUpdates[0].update).toEqual({
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: `\n\n> **Fast mode turned off:** ${explanation}.\n\n`,
},
});
expect(sessionUpdates[1].update).toMatchObject({ sessionUpdate: "config_option_update" });
expect(session.configOptions).toContainEqual(createFastModeConfigOption(false, true, reason));
// The same report again changes nothing the user can see: no repeat notice.
await sync(SESSION_ID, session, "off", reason);
expect(sessionUpdates).toHaveLength(2);
},
);
/**
* Scenario:
* 1. The toggle is on and the SDK reports `off` with `sdk_opt_in_required`.
* Expected:
* - The toggle is cleared and no reason is retained.
* - Only a `config_option_update` is sent: no notice or transcript line.
*/
it("clears the toggle quietly when the SDK reports off with sdk_opt_in_required", async () => {
const { sync, session, sessionUpdates } = setup({ fastModeEnabled: true, withOption: true });
await sync(SESSION_ID, session, "off", "extra_usage_disabled");
await sync(SESSION_ID, session, "off", "sdk_opt_in_required");
expect(session.fastModeEnabled).toBe(false);
expect(session.fastModeDisabledReason).toBe("extra_usage_disabled");
expect(sessionUpdates).toHaveLength(2);
expect(sessionUpdates[0].update).toEqual({
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: "\n\n> **Fast mode turned off:** requires extra usage to be enabled for this account.\n\n",
},
});
expect(sessionUpdates[1].update).toMatchObject({ sessionUpdate: "config_option_update" });
expect(session.configOptions).toContainEqual(
createFastModeConfigOption(false, true, "extra_usage_disabled"),
);
// The same report again changes nothing the user can see: no repeat notice.
await sync(SESSION_ID, session, "off", "extra_usage_disabled");
expect(sessionUpdates).toHaveLength(2);
expect(session.fastModeDisabledReason).toBeUndefined();
expect(sessionUpdates.map((n) => n.update.sessionUpdate)).toEqual(["config_option_update"]);
});
it("explains the flip as a live notice to a client on the notice contract", async () => {
@@ -493,3 +568,89 @@ describe("syncFastModeState (SDK-driven state changes)", () => {
expect(disabledCase.sessionUpdates).toHaveLength(0);
});
});
describe("Fast mode that Claude Code refuses during a turn", () => {
const SESSION_ID = "test-session";
/** A session whose Claude Code answers the first prompt with a `system`/init
* that reports Fast mode off with `reason`, as it does on every turn. */
async function setupTurn(reason: FastModeDisabledReason) {
const updates: SessionNotification[] = [];
const client = {
sessionUpdate: async (n: SessionNotification) => {
updates.push(n);
},
} as unknown as AcpClient;
const agent = new ClaudeAcpAgent(client, { log: () => {}, error: () => {} });
await initializeClient(agent, { session: { configOptions: { boolean: {} } } });
const input = new Pushable<any>();
async function* messages() {
const { value: prompt } = await input[Symbol.asyncIterator]().next();
yield userEcho(prompt);
yield {
type: "system",
subtype: "init",
fast_mode_state: "off",
fast_mode_disabled_reason: reason,
session_id: SESSION_ID,
};
yield successfulResultMessage();
yield { type: "system", subtype: "session_state_changed", state: "idle" };
}
const query = Object.assign(wrapQuery(messages()), { applyFlagSettings: async () => {} });
agent.sessions[SESSION_ID] = mockSessionState({
query,
input,
fastModeEnabled: false,
configOptions: [createFastModeConfigOption(false, true)],
});
return { agent, updates };
}
/**
* Scenario:
* 1. The client, without the notices capability, turns Fast mode on.
* 2. It sends a prompt, and Claude Code's init reports Fast mode off, reason `preference`.
* Expected:
* - The transcript gets one line that explains the organization or gateway has not enabled it.
* - Then one `config_option_update` where `fast` reads off, with the explanation in its description.
*/
it("explains a Fast mode that the organization or gateway has not enabled", async () => {
const { agent, updates } = await setupTurn("preference");
await agent.setSessionConfigOption({
sessionId: SESSION_ID,
configId: "fast",
type: "boolean",
value: true,
});
updates.length = 0;
await agent.prompt({ sessionId: SESSION_ID, prompt: [{ type: "text", text: "hi" }] });
const explained = updates
.map((n) => n.update)
.filter((u) => u.sessionUpdate !== "usage_update");
expect(explained).toEqual([
{
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: "\n\n> **Fast mode turned off:** not enabled for your organization or API gateway.\n\n",
},
},
{
sessionUpdate: "config_option_update",
configOptions: [
expect.objectContaining({
id: "fast",
currentValue: false,
description: expect.stringMatching(
/ — not enabled for your organization or API gateway$/,
),
}),
],
},
]);
});
});

View File

@@ -6,7 +6,7 @@ import * as path from "node:path";
import * as os from "node:os";
import { messageIdForGrouping } from "../../sdk-to-acp.js";
import { forkSession } from "../../fork-session.js";
import { encodeProjectPath } from "../../v1/session-index/project-dirs.js";
import { encodeProjectPath } from "../../session-index/project-dirs.js";
// Use the real SDK against temporary transcripts, without starting Claude Code.
let root: string;

View File

@@ -226,7 +226,10 @@ function scriptClaudeCode({
};
}
async function connect(clientCapabilities: Record<string, unknown> = {}) {
async function connect(
clientCapabilities: Record<string, unknown> = {},
sessionMeta?: Record<string, unknown>,
) {
vi.resetModules();
const { ClaudeAcpAgent } = await import("../../v1/acp-agent.js");
const updates: Array<{ update: Record<string, any> }> = [];
@@ -241,7 +244,7 @@ async function connect(clientCapabilities: Record<string, unknown> = {}) {
error: () => {},
});
await initializeClient(agent, clientCapabilities);
const session = await agent.newSession({ cwd, mcpServers: [] });
const session = await agent.newSession({ cwd, mcpServers: [], _meta: sessionMeta });
return { agent, sessionId: session.sessionId, updates, logs };
}
@@ -322,6 +325,22 @@ describe("idle compaction in the agent", () => {
await agent.dispose();
});
/** The client's `settings` option reaches the CLI as `--settings`, and idle compaction reads it too. */
it("does not compact a session whose client turned idle compaction off in the settings option", async () => {
const claude = scriptClaudeCode();
const { agent, sessionId } = await connect(
{},
{ claudeCode: { options: { settings: { idleCompaction: false } } } },
);
await promptHi(agent, sessionId);
await claude.untilIdle();
await vi.advanceTimersByTimeAsync(2 * 60 * MINUTE_MS);
expect(claude.received).toEqual(["hi"]);
await agent.dispose();
});
/** Closing the session during the wait drops the timer. */
it("does not compact once the session is closed", async () => {
const claude = scriptClaudeCode();

View File

@@ -23,7 +23,7 @@ import {
type InitializeResponse,
type SessionInfo,
} from "@agentclientprotocol/sdk/unstable";
import { encodeProjectPath } from "../../v1/session-index/project-dirs.js";
import { encodeProjectPath } from "../../session-index/project-dirs.js";
import {
archiveInsteadOfDelete,
archiveTo,
@@ -31,16 +31,16 @@ import {
parseBatchGetSessionsRequest,
SessionIndexService,
writeCustomTitleSidecar,
} from "../../v1/session-index/service.js";
import { scanTranscriptFile } from "../../v1/session-index/transcript-scan.js";
import { repositoryWorktrees } from "../../v1/session-index/worktrees.js";
} from "../../session-index/service.js";
import { scanTranscriptFile } from "../../session-index/transcript-scan.js";
import { repositoryWorktrees } from "../../session-index/worktrees.js";
import {
ListSubscriptions,
MAX_SUBSCRIPTIONS,
parseListSubscribeRequest,
parseListUnsubscribeRequest,
type ListChanges,
} from "../../v1/session-index/list-subscriptions.js";
} from "../../session-index/list-subscriptions.js";
import { SessionTitles } from "../../session-titles.js";
import { initializeClient } from "../helpers.js";
import {
@@ -1350,6 +1350,48 @@ describe("_session/rename", () => {
});
});
describe("session/set_title", () => {
it("titles a stored session for a client without the session index", async () => {
const session = await writeTranscript({});
const { agent } = createAgent();
await initializeClient(agent, {});
expect(
await agent.setSessionTitle({ sessionId: session.id, title: " Investigate\n OAuth " }),
).toEqual({ title: "Investigate OAuth" });
expect(await lastRecords(session.file)).toEqual(titleRecords(session.id, "Investigate OAuth"));
const page = await agent.listSessions({ cwd: workspace });
expect(page.sessions[0]!.title).toBe("Investigate OAuth");
});
it.each([
["an AIR client with sessionIndex", air("sessionIndex"), "Renamed"],
["an AIR client without sessionIndex", air(), "Renamed"],
["a client that is not AIR", {}, "[archived] Renamed"],
])(
"keeps an archived session archived and answers the title %s shows",
async (_client, capabilities, shown) => {
const session = await writeTranscript({ trailer: titleRecords("", "[archived] Fix it") });
const { agent } = createAgent();
await initializeClient(agent, capabilities);
expect(await agent.setSessionTitle({ sessionId: session.id, title: "Renamed" })).toEqual({
title: shown,
});
expect(await lastRecords(session.file)).toEqual(
titleRecords(session.id, "[archived] Renamed"),
);
},
);
it.each([
["an unknown session", () => ({ sessionId: randomUUID(), title: "x" }), -32002],
["an empty title", () => ({ sessionId: randomUUID(), title: " \n " }), -32602],
])("refuses %s", async (_case, params, code) => {
const { agent } = createAgent();
await initializeClient(agent, {});
await expect(agent.setSessionTitle(params())).rejects.toMatchObject({ code });
});
});
describe("_session/archive and _session/unarchive", () => {
it("append AIR's archive title records, are idempotent and keep updatedAt", async () => {
const session = await writeTranscript({ prompt: "Fix the\nparser" });
@@ -1854,7 +1896,7 @@ describe("_session/list/subscribe", () => {
});
it("clears the failed turn of a session that runs here when a new turn starts", async () => {
const { noteSessionState } = await import("../../v1/session-index/connection.js");
const { noteSessionState } = await import("../../v1/session-index.js");
const session = { lastTurnFailed: true } as any;
noteSessionState(session, "idle", "running");
expect(session.lastTurnFailed).toBe(false);

View File

@@ -5,7 +5,7 @@
* schema, so a malformed v2 message fails the test.
*/
import { randomUUID } from "node:crypto";
import { appendFile, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { appendFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -35,6 +35,7 @@ import {
v1SetSessionConfigOptionRequest,
v2ConfigOptions,
} from "../../v2/session-requests.js";
import { encodeProjectPath } from "../../session-index/project-dirs.js";
import { startupFailure } from "../helpers.js";
import packageJson from "../../../package.json" with { type: "json" };
@@ -108,6 +109,8 @@ const sdk = vi.hoisted(() => ({
accountInfo: async (): Promise<Record<string, unknown>> => ({}),
/** The query switches its model (`setModel`). */
setModel: async (_model?: string): Promise<void> => {},
/** Claude Code stores a session title (`rename_session`). */
renameSession: async (_title: string, _sessionId?: string): Promise<void> => {},
/** Set to serve Remote Control, as a query of a CLI that supports it does. */
enableRemoteControl: undefined as
((enabled: boolean, name?: string) => Promise<{ session_url?: string }>) | undefined,
@@ -142,6 +145,7 @@ vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
accountInfo: () => sdk.accountInfo(),
setMcpServers: (servers: Record<string, unknown>) => sdk.setMcpServers(servers),
setModel: (model?: string) => sdk.setModel(model),
renameSession: (title: string, sessionId?: string) => sdk.renameSession(title, sessionId),
...(sdk.enableRemoteControl ? { enableRemoteControl: sdk.enableRemoteControl } : {}),
});
return Object.assign(messages, controls);
@@ -395,6 +399,8 @@ describe("ACP protocol routing", () => {
additionalDirectories: {},
systemPrompt: { mode: { override: {} } },
status: {},
options: {},
title: {},
});
expect(response.capabilities?.providers).toEqual({});
// Not `_session/steering`, which v1 advertises in the top-level `_meta`.
@@ -883,6 +889,101 @@ describe("ACP v2 sessions", () => {
});
});
describe("session/set_title", () => {
it("titles an active session through Claude Code and publishes the stored title", async () => {
const renameSession = vi.fn(sdkDefaults.renameSession);
sdk.renameSession = renameSession;
const client = v2Client();
await client.app.connectWith(connectRouter(), async (agent) => {
await initializeV2(agent);
const { sessionId } = await agent.request(v2.methods.agent.session.new, { cwd });
client.sessionUpdates.length = 0;
const response = await agent.request(v2.methods.agent.session.setTitle, {
sessionId,
title: " Investigate\n OAuth timeout ",
});
expect(response).toEqual({ title: "Investigate OAuth timeout" });
expect(renameSession).toHaveBeenCalledWith("Investigate OAuth timeout", sessionId);
expect(client.sessionUpdates).toEqual([
{
sessionId,
update: { sessionUpdate: "session_info_update", title: "Investigate OAuth timeout" },
},
]);
await client.authUpdate(1);
});
});
it("titles the query that a resume started while the rename ran", async () => {
let release!: () => void;
const renameSession = vi.fn(async (_title: string, _sessionId?: string) => {
if (renameSession.mock.calls.length === 1) {
await new Promise<void>((resolve) => (release = resolve));
}
});
sdk.renameSession = renameSession;
const client = v2Client();
await client.app.connectWith(connectRouter(), async (agent) => {
await initializeV2(agent);
const { sessionId } = await agent.request(v2.methods.agent.session.new, { cwd });
const renamed = agent.request(v2.methods.agent.session.setTitle, {
sessionId,
title: "Mine",
});
await vi.waitFor(() => expect(renameSession).toHaveBeenCalledTimes(1));
await agent.request(v2.methods.agent.session.close, { sessionId });
await agent.request(v2.methods.agent.session.resume, { sessionId, cwd });
release();
expect(await renamed).toEqual({ title: "Mine" });
expect(renameSession.mock.calls).toEqual([
["Mine", sessionId],
["Mine", sessionId],
]);
await client.authUpdate(1);
});
});
it("titles a stored session that is not active in its transcript", async () => {
const sessionId = randomUUID();
const dir = path.join(tempDir, "claude", "projects", encodeProjectPath(cwd));
await mkdir(dir, { recursive: true });
const file = path.join(dir, `${sessionId}.jsonl`);
const prompt = { type: "user", sessionId, cwd, message: { role: "user", content: "hi" } };
await writeFile(file, JSON.stringify(prompt) + "\n");
const { app, authUpdate } = v2Client();
await app.connectWith(connectRouter(), async (agent) => {
await initializeV2(agent);
expect(
await agent.request(v2.methods.agent.session.setTitle, { sessionId, title: "Stored" }),
).toEqual({ title: "Stored" });
await authUpdate(1);
});
const records = (await readFile(file, "utf8"))
.trim()
.split("\n")
.slice(-2)
.map((line) => JSON.parse(line));
expect(records).toEqual([
{ type: "custom-title", customTitle: "Stored", sessionId },
{ type: "agent-name", agentName: "Stored", sessionId },
]);
});
it.each([
["an unknown session", { sessionId: randomUUID(), title: "Title" }, -32002],
["a title that is empty once normalized", { sessionId: randomUUID(), title: " \n " }, -32602],
])("refuses %s", async (_case, params, code) => {
const { app, authUpdate } = v2Client();
await app.connectWith(connectRouter(), async (agent) => {
await initializeV2(agent);
await expect(
agent.request(v2.methods.agent.session.setTitle, params),
).rejects.toMatchObject({ code });
await authUpdate(1);
});
});
});
it("offers a resumed Fable 5.2 session's model as `fable`, as a new session lists it", async () => {
// On resume, the CLI lists the transcript's Fable row under its id, with no `fable` row.
const row = (value: string, resolvedModel: string, displayName: string) => ({
@@ -1344,6 +1445,51 @@ describe("ACP v2 sessions", () => {
});
});
/**
* Scenario:
* 1. The client turns Fast mode on and sends a prompt.
* 2. Claude Code's `system`/init of that turn reports Fast mode off, reason `preference`.
* Expected:
* - One warning notice explains that the organization or gateway has not enabled it.
* - The `fast` option reads off, and its description ends with the explanation.
*/
it("explains a Fast mode that the organization or gateway has not enabled", async () => {
scriptTurns(async function* (options) {
yield fastModeInit(options, "off", "preference");
yield assistantText(options, "Done.");
yield result(options);
yield idleState(options);
});
const client = v2Client();
await client.app.connectWith(connectRouter(), async (agent) => {
await initializeV2(agent);
const { sessionId } = await agent.request(v2.methods.agent.session.new, { cwd });
await agent.request(v2.methods.agent.session.setConfigOption, {
sessionId,
configId: "fast",
type: "boolean",
value: true,
});
await agent.request(v2.methods.agent.session.prompt, { sessionId, prompt: text("hi") });
await vi.waitFor(() => expect(turnTrace(client.sessionUpdates)).toContain("idle end_turn"));
await client.authUpdate(1);
});
expect(client.updates("notice")).toEqual([
{
sessionUpdate: "notice",
severity: "warning",
title: "Fast mode turned off",
description: "Not enabled for your organization or API gateway.",
},
]);
expect(fastOption(client.updates("config_option_update").at(-1))).toMatchObject({
currentValue: false,
description:
"Faster responses on supported models — not enabled for your organization or API gateway",
});
});
it("reports a refusal fallback with a notice and follows the model it swapped to", async () => {
sdk.initializationResult = async () => ({ models: [OPUS, HAIKU] });
scriptTurns(async function* (options) {
@@ -1590,6 +1736,23 @@ function stream(options: Options, event: Record<string, unknown>) {
};
}
/** Claude Code's per-turn `system`/init, reporting the Fast mode state. */
function fastModeInit(options: Options, state: string, reason: string) {
return {
type: "system",
subtype: "init",
fast_mode_state: state,
fast_mode_disabled_reason: reason,
session_id: options.sessionId,
uuid: randomUUID(),
};
}
/** The `fast` option of a `config_option_update`. */
function fastOption(update: { configOptions: { configId: string }[] } | undefined) {
return update?.configOptions.find((option) => option.configId === "fast");
}
function idleState(options: Options) {
return {
type: "system",
@@ -4111,47 +4274,60 @@ describe("ACP v2 idle compaction", () => {
},
});
/**
* Prompts "hi" in a session created with `meta`, answers it with a large
* one-hour request, and waits 54 minutes. Returns the prompts Claude Code
* received and the client.
*/
async function promptAndIdle(meta?: Record<string, unknown>) {
const received: string[] = [];
sdk.run = async function* (prompts, options) {
const input = prompts[Symbol.asyncIterator]();
for (;;) {
const { value, done } = await input.next();
if (done) return;
received.push((value.message.content as Array<{ text: string }>)[0].text);
if (received.length === 1) {
yield {
type: "user",
message: value.message,
parent_tool_use_id: null,
uuid: value.uuid,
session_id: options.sessionId,
isReplay: true,
};
yield largeOneHourRequest(options);
} else {
yield { type: "system", subtype: "status", status: "compacting", uuid: randomUUID() };
}
yield result(options);
yield idleState(options);
}
};
const client = v2Client();
await client.app.connectWith(connectRouter(), async (agent) => {
await initializeV2(agent);
const { sessionId } = await agent.request(v2.methods.agent.session.new, {
cwd,
_meta: meta,
});
await agent.request(v2.methods.agent.session.prompt, { sessionId, prompt: text("hi") });
await vi.advanceTimersByTimeAsync(0);
await vi.advanceTimersByTimeAsync(54 * 60_000);
for (let i = 0; i < 20 && received.length < 2; i++) await vi.advanceTimersByTimeAsync(0);
for (let i = 0; i < 20 && turnTrace(client.sessionUpdates).length < 6; i++) {
await vi.advanceTimersByTimeAsync(0);
}
await client.authUpdate(1);
});
return { received, client };
}
/** The session is idle for 54 minutes after a large one-hour request, and the adapter's `/compact` runs. */
it("reports the compaction that the adapter starts as running, then idle, with no user_message", async () => {
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "Date"] });
try {
const received: string[] = [];
sdk.run = async function* (prompts, options) {
const input = prompts[Symbol.asyncIterator]();
for (;;) {
const { value, done } = await input.next();
if (done) return;
received.push((value.message.content as Array<{ text: string }>)[0].text);
if (received.length === 1) {
yield {
type: "user",
message: value.message,
parent_tool_use_id: null,
uuid: value.uuid,
session_id: options.sessionId,
isReplay: true,
};
yield largeOneHourRequest(options);
} else {
yield { type: "system", subtype: "status", status: "compacting", uuid: randomUUID() };
}
yield result(options);
yield idleState(options);
}
};
const client = v2Client();
await client.app.connectWith(connectRouter(), async (agent) => {
await initializeV2(agent);
const { sessionId } = await agent.request(v2.methods.agent.session.new, { cwd });
await agent.request(v2.methods.agent.session.prompt, { sessionId, prompt: text("hi") });
await vi.advanceTimersByTimeAsync(0);
await vi.advanceTimersByTimeAsync(54 * 60_000);
for (let i = 0; i < 20 && received.length < 2; i++) await vi.advanceTimersByTimeAsync(0);
for (let i = 0; i < 20 && turnTrace(client.sessionUpdates).length < 6; i++) {
await vi.advanceTimersByTimeAsync(0);
}
await client.authUpdate(1);
});
const { received, client } = await promptAndIdle();
expect(received).toEqual(["hi", "/compact"]);
expect(turnTrace(client.sessionUpdates)).toEqual([
@@ -4166,6 +4342,20 @@ describe("ACP v2 idle compaction", () => {
vi.useRealTimers();
}
});
/** The client's `settings` option reaches the CLI as `--settings`, and idle compaction reads it too. */
it("does not compact a session whose client turned idle compaction off in the settings option", async () => {
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "Date"] });
try {
const { received } = await promptAndIdle({
claudeCode: { options: { settings: { idleCompaction: false } } },
});
expect(received).toEqual(["hi"]);
} finally {
vi.useRealTimers();
}
});
});
// Claude Code runs model cycles that no prompt started, for example its

View File

@@ -9,7 +9,7 @@
* `session_state_changed`.
*/
import { randomUUID } from "node:crypto";
import { mkdtemp, realpath, rm } from "node:fs/promises";
import { mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -24,6 +24,13 @@ import { acpProtocolRouter } from "../../serve.js";
import { SettingsManager } from "../../settings.js";
import type { AgentHandle } from "../../v2/agent.js";
import { StoppedWork } from "../../v2/stopped-work.js";
import {
transcriptAssistant,
transcriptCompactBoundary,
transcriptCompactSummary,
transcriptUser,
writeTranscript,
} from "../helpers.js";
/** One Claude Code process: the prompts it reads and the messages it sends. */
type FakeClaudeCode = {
@@ -489,12 +496,58 @@ describe("ACP v2 core", () => {
expect(response.protocolVersion).toBe(2);
expect(response.capabilities?.session?.fork).toEqual({});
expect(response.capabilities?.session?.options).toEqual({});
expect(response.capabilities?.session?.prompt).toEqual({ image: {}, embeddedContext: {} });
expect((response.authMethods ?? []).map((method) => method.methodId)).toContain(
"console-login",
);
});
it.each(["environment", "caller object", "caller file"])(
"includes configured models omitted by the SDK from %s settings",
async (source) => {
const configured = {
availableModels: ["claude-haiku-4-5", "claude-haiku-5-5"],
modelOverrides: {
"claude-haiku-4-5": "global.anthropic.claude-haiku-4-5-20251001-v1:0",
"claude-haiku-5-5": "global.anthropic.claude-haiku-5-5",
},
};
let settings: typeof configured | string | undefined;
if (source === "environment") {
vi.stubEnv("CLAUDE_MODEL_CONFIG", JSON.stringify(configured));
} else if (source === "caller file") {
settings = "caller-settings.json";
await writeFile(path.join(cwd, settings), JSON.stringify(configured));
} else {
settings = configured;
}
const client = v2Client();
await client.connect(async (agent) => {
await agent.request(v2.methods.agent.initialize, {
protocolVersion: v2.PROTOCOL_VERSION,
info: { name: "v2-test-client", version: "1" },
});
const response = await agent.request(v2.methods.agent.session.new, {
cwd,
_meta: { claudeCode: { options: { settings } } },
});
const ids = ["default", ...Object.values(configured.modelOverrides)];
const model = response.configOptions?.find((option) => option.configId === "model");
expect(model).toMatchObject({
options: ids.map((value) => expect.objectContaining({ value })),
});
await agent.request(v2.methods.agent.session.setConfigOption, {
sessionId: response.sessionId,
configId: "model",
type: "id",
value: ids[2],
} as v2.SetSessionConfigOptionRequest);
expect(sdk.processes[0].setModel).toHaveBeenLastCalledWith(ids[2]);
});
},
);
it("routes the sessions started after a gateway login through the gateway", async () => {
const client = v2Client();
const baseUrl = "https://gateway.example.com";
@@ -1012,6 +1065,30 @@ describe("ACP v2 core", () => {
});
});
it("stays idle at the echo of a model change's local command output", async () => {
const client = v2Client();
await client.connect(async (agent) => {
const { claude } = await openSession(agent);
// As the bundled CLI sends it after `setModel`, with no turn: no
// `command_lifecycle` frame, result or `session_state_changed` follows.
claude.send({
type: "user",
message: {
role: "user",
content:
"<local-command-stdout>Set model to `haiku (claude-haiku-5-5)`</local-command-stdout>",
},
parent_tool_use_id: null,
uuid: randomUUID(),
session_id: "session",
timestamp: new Date().toISOString(),
isReplay: true,
});
await new Promise((resolve) => setTimeout(resolve, 20));
expect(client.states()).toEqual([]);
});
});
it("forwards the result of a local command, which streamed no text", async () => {
const client = v2Client();
await client.connect(async (agent) => {
@@ -1736,6 +1813,234 @@ describe("ACP v2 core", () => {
expect(error).toMatchObject({ code: -32602 });
});
describe("session options", () => {
const STORED = "7f1c5b0e-3c53-4c0c-9d0a-0d6c1f0a2b11";
const values = (options: v2.SessionConfigOption[] | undefined) =>
Object.fromEntries((options ?? []).map((o) => [o.configId, o.currentValue]));
const initialize = (agent: v2.ClientContext) =>
agent.request(v2.methods.agent.initialize, {
protocolVersion: v2.PROTOCOL_VERSION,
info: { name: "v2-test-client", version: "1.0.0" },
capabilities: {},
});
beforeEach(() => {
sdk.models = [
{
value: "default",
displayName: "Default",
description: "Recommended",
supportsAutoMode: true,
supportsEffort: true,
supportedEffortLevels: ["low", "medium", "high"],
},
{
value: "haiku",
displayName: "Haiku",
description: "Fast",
supportsAutoMode: false,
supportsEffort: true,
supportedEffortLevels: ["low", "medium"],
},
];
});
it("answers session/options with what session/new returns, from a query that it closes", async () => {
const client = v2Client();
const { options, created } = await client.connect(async (agent) => {
await initialize(agent);
const options = await agent.request(v2.methods.agent.session.options, {
cwd,
additionalDirectories: ["/extra"],
});
const created = await agent.request(v2.methods.agent.session.new, { cwd });
return { options, created };
});
const [probe, session] = sdk.processes;
expect(probe.options).toMatchObject({
cwd,
additionalDirectories: ["/extra"],
mcpServers: {},
persistSession: false,
});
expect(probe.close).toHaveBeenCalledTimes(1);
expect(session.options.persistSession).toBeUndefined();
const { sessionId, ...fields } = created;
expect(sessionId).not.toBe(probe.options.sessionId);
expect(options).toEqual(fields);
expect(options.availableCommands?.map((command) => command.name)).toContain("review");
expect(client.notifications).toEqual([]);
});
it("answers session/options of an active session with its current options, without a query", async () => {
const client = v2Client();
const { options, configOptions, wrongCwd } = await client.connect(async (agent) => {
const { response } = await openSession(agent);
const { sessionId } = response;
const { configOptions } = await agent.request(v2.methods.agent.session.setConfigOption, {
sessionId,
configId: "model",
type: "id",
value: "haiku",
});
const options = await agent.request(v2.methods.agent.session.options, { cwd, sessionId });
const wrongCwd = await agent
.request(v2.methods.agent.session.options, { cwd: tempDir, sessionId })
.catch((error: unknown) => error);
return { options, configOptions, wrongCwd };
});
expect(sdk.processes).toHaveLength(1);
expect(options.configOptions).toEqual(configOptions);
expect(values(options.configOptions).model).toBe("haiku");
expect(wrongCwd).toMatchObject({ code: -32602 });
});
it("answers session/options of a stored session from a query that resumes it", async () => {
const client = v2Client();
const status = await client.connect(async (agent) => {
await initialize(agent);
await agent.request(v2.methods.agent.session.options, { cwd, sessionId: STORED });
return agent
.request(v2.methods.agent.session.status, { sessionId: STORED })
.catch((error: unknown) => error);
});
const [probe] = sdk.processes;
expect(probe.options).toMatchObject({ resume: STORED, persistSession: false });
expect(probe.options.sessionId).toBeUndefined();
expect(probe.close).toHaveBeenCalledTimes(1);
// The session is not active: it has no transcript in this test.
expect(status).toMatchObject({ code: -32002 });
});
it("fails session/options of an unknown session with -32002", async () => {
sdk.startFailure = new Error(`No conversation found with session ID: ${STORED}`);
const client = v2Client();
const error = await client.connect(async (agent) => {
await initialize(agent);
return agent
.request(v2.methods.agent.session.options, { cwd, sessionId: STORED })
.catch((error: unknown) => error);
});
expect(error).toMatchObject({ code: -32002, data: { sessionId: STORED } });
expect(sdk.processes[0].close).toHaveBeenCalled();
});
it("starts a new session with the request's configOptions, in list order, without an update", async () => {
const client = v2Client();
const response = await client.connect(async (agent) => {
await initialize(agent);
return agent.request(v2.methods.agent.session.new, {
cwd,
configOptions: [
// Skipped: an unknown option, a boolean for a select, and a value not offered.
{ configId: "nope", type: "id", value: "x" },
{ configId: "ultracode", type: "boolean", value: true },
{ configId: "model", type: "id", value: "haiku" },
// Haiku offers no "high": the entry is checked against the options the model left.
{ configId: "effort", type: "id", value: "high" },
{ configId: "mode", type: "id", value: "plan" },
],
});
});
const [claude] = sdk.processes;
expect(claude.setModel).toHaveBeenCalledWith("haiku");
expect(claude.setPermissionMode).toHaveBeenCalledWith("plan");
expect(values(response.configOptions)).toMatchObject({ model: "haiku", mode: "plan" });
expect(values(response.configOptions).effort).not.toBe("high");
expect(client.notifications).toEqual([]);
});
it.each<[string, v2.InitialSessionConfigOption[]]>([
[
"two entries of one configId",
[
{ configId: "mode", type: "id", value: "plan" },
{ configId: "mode", type: "id", value: "default" },
],
],
["a value type it does not take", [{ configId: "mode", type: "_custom", value: 1 }]],
])("rejects %s before it starts a query", async (_name, configOptions) => {
const client = v2Client();
const errors = await client.connect(async (agent) => {
await initialize(agent);
return Promise.all(
[
agent.request(v2.methods.agent.session.new, { cwd, configOptions }),
agent.request(v2.methods.agent.session.resume, {
sessionId: STORED,
cwd,
configOptions,
}),
].map((request) => request.catch((error: unknown) => error)),
);
});
expect(errors).toEqual([
expect.objectContaining({ code: -32602 }),
expect.objectContaining({ code: -32602 }),
]);
expect(sdk.processes).toHaveLength(0);
});
it("sets the configOptions of a session/resume on the session, an active one too", async () => {
const client = v2Client();
const { started, active } = await client.connect(async (agent) => {
await initialize(agent);
const resume = (configOptions: v2.InitialSessionConfigOption[]) =>
agent.request(v2.methods.agent.session.resume, { sessionId: STORED, cwd, configOptions });
const started = await resume([{ configId: "mode", type: "id", value: "plan" }]);
const active = await resume([{ configId: "model", type: "id", value: "haiku" }]);
return { started, active };
});
expect(sdk.processes).toHaveLength(1);
expect(values(started.configOptions).mode).toBe("plan");
expect(values(active.configOptions)).toMatchObject({ mode: "plan", model: "haiku" });
expect(client.sessionUpdates.map((update) => update.sessionUpdate)).not.toContain(
"config_option_update",
);
});
it("leaves the notice of an Auto mode fallback for the first prompt", async () => {
const client = v2Client();
const { response, before } = await client.connect(async (agent) => {
await initialize(agent);
const response = await agent.request(v2.methods.agent.session.new, {
cwd,
configOptions: [
{ configId: "mode", type: "id", value: "auto" },
// Haiku cannot serve Auto.
{ configId: "model", type: "id", value: "haiku" },
],
});
const before = [...client.sessionUpdates];
const claude = sdk.processes[0];
const prompted = agent.request(v2.methods.agent.session.prompt, {
sessionId: response.sessionId,
prompt: PROMPT,
});
const prompt = await claude.prompt(0);
claude.send(lifecycle(prompt, "started"));
claude.send(echo(prompt));
claude.send(result());
claude.send(sessionState("idle"));
await prompted;
return { response, before };
});
expect(values(response.configOptions)).toMatchObject({ mode: "acceptEdits", model: "haiku" });
expect(before).toEqual([]);
expect(client.sessionUpdates.filter((update) => update.sessionUpdate === "notice")).toEqual([
expect.objectContaining({ title: "Auto mode unavailable" }),
]);
});
});
it("rejects additional directories that are not absolute", async () => {
const client = v2Client();
const error = await client.connect(async (agent) => {
@@ -1895,6 +2200,49 @@ describe("ACP v2 core", () => {
]);
});
it("replays the messages before a compaction from the transcript", async () => {
const sessionId = randomUUID();
// The records of `/compact` between two prompts, as Claude Code writes
// them: the boundary has no parent and links "Hello" logically. The SDK
// looks the project up by the real path of `cwd` (`/private/var` on macOS).
const project = (await realpath(cwd)).replace(/[^a-zA-Z0-9]/g, "-");
await writeTranscript(path.join(tempDir, "claude", "projects", project), sessionId, [
transcriptUser("u1", "hi"),
transcriptAssistant("a1", "Hello"),
transcriptCompactBoundary("b1", "a1"),
transcriptCompactSummary("s1"),
transcriptUser("u2", "identify yourself"),
]);
// The transcript read and the read across the compaction both go to disk.
const actual = await vi.importActual<typeof import("@anthropic-ai/claude-agent-sdk")>(
"@anthropic-ai/claude-agent-sdk",
);
vi.mocked(getSessionMessages)
.mockImplementationOnce(actual.getSessionMessages)
.mockImplementationOnce(actual.getSessionMessages);
const client = v2Client();
await client.connect(async (agent) => {
await agent.request(v2.methods.agent.initialize, {
protocolVersion: v2.PROTOCOL_VERSION,
info: { name: "v2-test-client", version: "1.0.0" },
capabilities: {},
});
await agent.request(v2.methods.agent.session.resume, {
sessionId,
cwd,
replayFrom: { type: "start" },
});
});
expect(
client.sessionUpdates.flatMap((update) =>
update.sessionUpdate === "user_message" || update.sessionUpdate === "compaction_update"
? [update.sessionUpdate]
: [],
),
).toEqual(["user_message", "compaction_update", "user_message"]);
});
it.each([
[
"a prompt that Claude Code started after the cancel answered it",
@@ -2085,7 +2433,7 @@ describe("ACP v2 core", () => {
expect(client.sessionUpdates.at(-1)).toMatchObject({
state: "idle",
stopReason: "error",
error: { code: -32000 },
error: { code: -32000, message: "Authentication required" },
});
});
@@ -2094,7 +2442,7 @@ describe("ACP v2 core", () => {
"an authentication failure as authRequired",
"authentication_failed",
"Failed to authenticate. API Error: 401",
{ code: -32000 },
{ code: -32000, message: "Authentication required: Failed to authenticate. API Error: 401" },
],
[
"another failure with its error kind",
@@ -2157,6 +2505,53 @@ describe("ACP v2 core", () => {
},
);
it("reports a failed API request that the turn goes on past where it failed", async () => {
const text = "API Error: Connection lost mid-response. The response above may be incomplete.";
const client = v2Client();
await client.connect(async (agent) => {
const { claude } = await openSession(agent);
claude.send(cycleStart());
// Claude Code keeps the tool call of the response that broke off, and runs it.
claude.send(
assistant("msg_partial", [
{ type: "tool_use", id: "toolu_kept", name: "Bash", input: { command: "ls" } },
]),
);
claude.send({
...assistant("msg_api_error", [{ type: "text", text }]),
message: {
id: "msg_api_error",
type: "message",
role: "assistant",
model: "<synthetic>",
content: [{ type: "text", text }],
usage: {},
},
error: "server_error",
});
claude.send({
type: "user",
message: {
role: "user",
content: [{ type: "tool_result", tool_use_id: "toolu_kept", content: "src" }],
},
parent_tool_use_id: null,
uuid: randomUUID(),
});
claude.send(assistant("msg_after", [{ type: "text", text: "Done." }]));
claude.send(result({ result: "Done." }));
await vi.waitFor(() => expect(client.states()).toEqual(["running", "idle"]));
});
const notice = { sessionUpdate: "notice", severity: "error", title: text };
const shown = client.sessionUpdates.filter(
(update) =>
update.sessionUpdate === "notice" || update.sessionUpdate === "agent_message_chunk",
);
expect(shown).toMatchObject([notice, { sessionUpdate: "agent_message_chunk" }]);
expect(shown[0]).toEqual(notice);
});
it.each([
[
"an error at the turn limit",

View File

@@ -15,6 +15,7 @@ import {
ForkSessionResponse,
InitializeRequest,
InitializeResponse,
InitialSessionConfigOption,
ListProvidersRequest,
ListProvidersResponse,
McpServer,
@@ -43,11 +44,15 @@ import {
SetSessionConfigOptionResponse,
SetSessionModeRequest,
SetSessionModeResponse,
SessionOptionsRequest,
SessionOptionsResponse,
SessionStatusRequest,
SessionStatusResponse,
CloseSessionRequest,
CloseSessionResponse,
DeleteSessionRequest,
SetSessionTitleRequest,
SetSessionTitleResponse,
DeleteSessionResponse,
WriteTextFileRequest,
WriteTextFileResponse,
@@ -94,6 +99,11 @@ import {
import { checklistPlanUpdate, resetPlanUpdates, type SubagentChecklists } from "../plans.js";
import { PromptSuggestions } from "../prompt-suggestions.js";
import { savedSessionStatus, SESSION_STATUS_CAPABILITY } from "../session-status.js";
import {
applyInitialConfigOptions,
initialConfigOptions,
SESSION_OPTIONS_CAPABILITY,
} from "../initial-config-options.js";
import { sanitizeTitle, SessionTitles } from "../session-titles.js";
import {
AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY,
@@ -133,16 +143,16 @@ import {
clientSupportsAirCapability,
withAirMeta,
} from "../air-extension.js";
import * as sessionIndex from "./session-index/connection.js";
import * as sessionIndex from "./session-index.js";
import type {
BatchGetSessionsRequest,
RenameSessionRequest,
SessionIdRequest,
} from "./session-index/service.js";
} from "../session-index/service.js";
import type {
ListSubscribeRequest,
ListUnsubscribeRequest,
} from "./session-index/list-subscriptions.js";
} from "../session-index/list-subscriptions.js";
import {
AsyncTaskRuntime,
backgroundBashTaskFromToolResult,
@@ -150,6 +160,20 @@ import {
clientSupportsAsyncTasks,
} from "../async-tasks.js";
import { SessionBackgroundWork } from "./background-work.js";
import {
ASYNC_TASK_STOP_METHOD,
parseAsyncTaskStopRequest,
type AsyncTaskStopRequest,
type AsyncTaskStopResponse,
} from "./async-task-stop.js";
import {
STEER_METHOD,
STEER_PRIORITY_LATER,
STEER_PRIORITY_NOW,
parseSteerRequest,
type SteerRequest,
type SteerResponse,
} from "./steering.js";
import {
AUTH_STATUS_UPDATE_METHOD,
type AuthStatus,
@@ -201,6 +225,7 @@ import {
replaySubagentTerminalState,
} from "../subagent-replay.js";
import {
readHistoryAcrossCompactions,
readResumedSession,
readResumedTail,
type ResumedSessionSnapshot,
@@ -218,6 +243,7 @@ import { decodeClaudePermissionResponse } from "../permissions/response.js";
import { SettingsManager } from "../settings.js";
import {
activeUsageLimitMessage,
apiRetryWarning,
airSessionFailureCapabilityMeta,
assistantMessageText,
type ClaudeFailureKind,
@@ -409,6 +435,7 @@ import {
stripLocalCommandMetadata,
} from "../message-filters.js";
import type { NewSessionMeta, SDKMessageFilter } from "../new-session-meta.js";
import { computeSessionFingerprint } from "./session-fingerprint.js";
import { promptToClaude, SUPPORTED_PROMPT_CONTENT_TYPES } from "../prompt-to-claude.js";
import { AUTONOMOUS_RESULT_ORIGINS, isAbortedResult } from "../result-end.js";
import {
@@ -565,112 +592,6 @@ const TURN_NO_RESULT_MESSAGE =
"The turn ended without a result: the agent went idle while this prompt was still in flight " +
"(e.g. the model stream dropped mid-turn). Any partial output may be incomplete; please retry.";
/** Custom (extension) request method a client uses to steer the turn that is
* currently running: the message is injected into the in-flight turn rather
* than queued as a separate `session/prompt`. Named `_session/steering` per the
* agreed ACP steering wire protocol; advertised to clients via the top-level
* `InitializeResponse._meta.steering.supported`. */
const STEER_METHOD = "_session/steering";
/** Stops one Claude background task without cancelling the parent prompt turn. */
const ASYNC_TASK_STOP_METHOD = "_session/async_task/stop";
type AsyncTaskStopRequest = {
sessionId: string;
asyncTaskId: string;
};
type AsyncTaskStopResponse = {
stopped: boolean;
};
function parseAsyncTaskStopRequest(value: unknown): AsyncTaskStopRequest {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw RequestError.invalidParams(undefined, "async task stop params must be an object");
}
const params = value as Record<string, unknown>;
const sessionId = typeof params.sessionId === "string" ? params.sessionId.trim() : "";
const asyncTaskId = typeof params.asyncTaskId === "string" ? params.asyncTaskId.trim() : "";
if (!sessionId) {
throw RequestError.invalidParams(
undefined,
"async task stop params require a non-empty sessionId",
);
}
if (!asyncTaskId) {
throw RequestError.invalidParams(
undefined,
"async task stop params require a non-empty asyncTaskId",
);
}
return { sessionId, asyncTaskId };
}
/** How urgently the SDK delivers a steered message relative to the running
* turn — an internal Claude implementation detail, not part of the wire
* contract. `now` pre-empts the current generation, while `later` waits for a
* pending permission/elicitation callback to settle instead of cancelling its
* ACP request and hiding the client's user-input card (IJAI-1191). */
const STEER_PRIORITY_NOW = "now" as const;
const STEER_PRIORITY_LATER = "later" as const;
/** Request-level steering options. `promptRequired` is opt-in so existing Hosts
* keep the established idle fallback behavior. */
type SteerMeta = {
[key: string]: unknown;
steering?: {
idleBehavior?: "promptRequired";
};
};
/** Params of a {@link STEER_METHOD} request. Shaped like the relevant subset of
* a `PromptRequest` so the same `promptToClaude` conversion applies. Delivery
* priority is deliberately NOT exposed here — it's an internal detail the agent
* chooses (see {@link STEER_PRIORITY}). */
export type SteerRequest = {
sessionId: string;
prompt: PromptRequest["prompt"];
_meta?: SteerMeta | null;
};
/** Result of a {@link STEER_METHOD} request. The legacy `startedNewTurn` result
* remains the default idle behavior; `promptRequired` is returned only when the
* Host explicitly opts into the host-owned fallback in request `_meta`. */
export type SteerResponse =
| { outcome: "injected" }
| { outcome: "startedNewTurn" }
| { outcome: "promptRequired"; reason: "noRunningTurn" };
/** Validate raw JSON-RPC params into a {@link SteerRequest}. Kept minimal — the
* content blocks are handed to `promptToClaude`, which tolerates unknown block
* types — but `sessionId` and a non-empty `prompt` array are required. */
function parseSteerRequest(params: unknown): SteerRequest {
if (!params || typeof params !== "object") {
throw RequestError.invalidParams(undefined, "steer params must be an object");
}
const { sessionId, prompt, _meta } = params as Record<string, unknown>;
if (typeof sessionId !== "string" || sessionId.length === 0) {
throw RequestError.invalidParams(undefined, "steer params require a non-empty sessionId");
}
if (!Array.isArray(prompt) || prompt.length === 0) {
throw RequestError.invalidParams(undefined, "steer params require a non-empty prompt array");
}
const steering =
_meta && typeof _meta === "object" ? (_meta as Record<string, unknown>).steering : undefined;
const idleBehavior =
steering && typeof steering === "object"
? (steering as Record<string, unknown>).idleBehavior
: undefined;
if (idleBehavior !== undefined && idleBehavior !== "promptRequired") {
throw RequestError.invalidParams(undefined, "unsupported steering idleBehavior");
}
return {
sessionId,
prompt: prompt as PromptRequest["prompt"],
_meta: _meta as SteerMeta | null | undefined,
};
}
/** Who started a turn. `idleCompaction` is the adapter's own `/compact` (see
* `IdleCompaction`): it is not a user prompt, so it neither disarms idle
* compaction, asks for a title from its prompt, nor clears the published prompt suggestion. */
@@ -1145,12 +1066,13 @@ export type Session = {
* independent of user ownership: opted-in clients also apply automatic
* recommendations and settings-derived values. */
appliedEffortLevel?: string;
/** Why the SDK currently can't serve Fast mode, when the reason is one worth
* telling the user about (see {@link FAST_MODE_UNAVAILABLE_EXPLANATIONS} —
* routine states like the SDK's own opt-in requirement normalize to
* `undefined`). Refreshed from every `fast_mode_disabled_reason` the SDK
* reports on `system`/init and user-turn `result`s; surfaced in the Fast mode
* option's description so a toggle that snaps back off explains itself. */
/** Why the SDK currently can't serve Fast mode: the last
* `fast_mode_disabled_reason` that `explainFastModeDisabledReason` explains.
* {@link normalizeFastModeDisabledReason} drops the two it doesn't,
* `sdk_opt_in_required` and `pending`, to `undefined`. Refreshed from every
* `fast_mode_disabled_reason` the SDK reports on `system`/init and user-turn
* `result`s; surfaced in the Fast mode option's description so a toggle that
* snaps back off explains itself. */
fastModeDisabledReason?: FastModeDisabledReason;
abortController: AbortController;
/** Signal the consumer races `query.next()` against. Aborted by cancel()
@@ -1542,149 +1464,6 @@ function disarmForceCancel(session: Session): void {
}
}
/** Normalize skills without changing their SDK semantics. Array order and
* duplicates do not affect the selected skill set, so neither should rebuild
* the underlying Query process. */
function normalizeSkills(skills: Options["skills"]): Options["skills"] {
return Array.isArray(skills) ? [...new Set(skills)].sort() : skills;
}
/** Whether a changed `_meta.claudeCode.options` key on session/load or
* session/resume must rebuild the Query process. Almost every option is read
* only when the process starts, so the default is `true`. Every key of the
* SDK's `Options` must be classified: an SDK bump that adds or removes an
* option fails to compile until it is decided here. */
const OPTION_REBUILDS_SESSION = {
additionalDirectories: true,
agentProgressSummaries: true,
agents: true,
allowDangerouslySkipPermissions: true,
allowedTools: true,
betas: true,
debug: true,
debugFile: true,
disallowedTools: true,
effort: true,
enableFileCheckpointing: true,
env: true,
executableArgs: true,
extraArgs: true,
fallbackModel: true,
forwardSubagentText: true,
includeHookEvents: true,
managedSettings: true,
maxBudgetUsd: true,
maxThinkingTokens: true,
maxTurns: true,
mcpServers: true,
model: true,
outputFormat: true,
pathToClaudeCodeExecutable: true,
permissionPrompts: true,
permissionPromptToolName: true,
persistSession: true,
perTaskStopAffordance: true,
planModeInstructions: true,
pluginDelivery: true,
plugins: true,
projectConfigRoot: true,
promptSuggestions: true,
sandbox: true,
sessionStoreFlush: true,
settings: true,
settingSources: true,
skills: true,
strictMcpConfig: true,
supportedDialogKinds: true,
systemPrompt: true,
taskBudget: true,
thinking: true,
toolAliases: true,
toolConfig: true,
tools: true,
verbatimPrompts: true,
// Managed or ignored by the adapter.
agent: false,
cwd: false,
executable: false,
includePartialMessages: false,
permissionMode: false,
// Per-call controls for attaching to the stored conversation.
continue: false,
forkSession: false,
resume: false,
resumeDropsTurn: false,
resumeSessionAt: false,
sessionId: false,
// Callbacks and live objects: they cannot cross JSON-RPC and have no stable serialized form.
abortController: false,
canUseTool: false,
hooks: false,
onElicitation: false,
onUserDialog: false,
sessionStore: false,
spawnClaudeCodeProcess: false,
stderr: false,
// No effect on an already-created session.
loadTimeoutMs: false,
title: false,
} as const satisfies Record<keyof Options, boolean>;
/** A JSON value with object keys sorted, so key order never changes a
* fingerprint. Functions and class instances (hook callbacks, abort
* controllers, in-process MCP servers) cannot cross JSON-RPC and have no
* stable serialized form, so they are left out. */
function canonicalJson(value: unknown): unknown {
if (typeof value === "function") return undefined;
if (value === null || typeof value !== "object") return value;
if (Array.isArray(value)) return value.map(canonicalJson);
const proto = Object.getPrototypeOf(value);
if (proto !== Object.prototype && proto !== null) return undefined;
return Object.fromEntries(
Object.keys(value)
.sort()
.map((key) => [key, canonicalJson((value as Record<string, unknown>)[key])])
.filter(([, entry]) => entry !== undefined),
);
}
/** Compute a stable fingerprint of the session-defining params so we can
* detect when a loadSession/resumeSession call requires tearing down and
* recreating the underlying Query process. Every option classified in
* {@link OPTION_REBUILDS_SESSION} is covered, so a warm resume never silently
* keeps stale values. MCP servers are sorted by name, and skills are
* normalized as a set, so ordering differences don't trigger unnecessary
* recreations. */
export function computeSessionFingerprint(params: {
cwd: string;
mcpServers?: NewSessionRequest["mcpServers"];
additionalDirectories?: NewSessionRequest["additionalDirectories"];
systemPrompt?: NewSessionRequest["systemPrompt"];
_meta?: NewSessionRequest["_meta"];
}): string {
const servers = [...(params.mcpServers ?? [])].sort((a, b) => a.name.localeCompare(b.name));
const meta = params._meta as (NewSessionMeta & Record<string, unknown>) | undefined;
const options: Record<string, unknown> = Object.fromEntries(
Object.entries(meta?.claudeCode?.options ?? {}).filter(
([key]) => (OPTION_REBUILDS_SESSION as Record<string, boolean>)[key] === true,
),
);
options.skills = normalizeSkills(options.skills as Options["skills"]);
return JSON.stringify(
canonicalJson({
cwd: params.cwd,
mcpServers: servers,
additionalDirectories: params.additionalDirectories,
additionalRoots: meta?.additionalRoots,
systemPrompt: meta?.systemPrompt,
clientSystemPrompt: params.systemPrompt ?? undefined,
disableBuiltInTools: meta?.disableBuiltInTools,
emitRawSDKMessages: meta?.claudeCode?.emitRawSDKMessages,
options,
}),
);
}
/** The JSON-RPC code ACP assigns to `authRequired`. Read from the SDK so it
* cannot drift from the errors this agent throws. */
const AUTH_REQUIRED_CODE = RequestError.authRequired().code;
@@ -2022,9 +1801,13 @@ export class ClaudeAcpAgent {
delete: {},
fork: {},
list: {},
// `session/options`, and `configOptions` on `session/new`, `load` and `resume`.
options: SESSION_OPTIONS_CAPABILITY,
resume: {},
status: SESSION_STATUS_CAPABILITY,
systemPrompt: SYSTEM_PROMPT_CAPABILITY,
// `session/set_title`.
title: {},
...(this.toolCallCapabilities.air.client ? { subagents: {} } : {}),
};
@@ -2105,15 +1888,77 @@ export class ClaudeAcpAgent {
}
async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
// Without `configOptions`: a restart of the query restores the session's choices itself.
const { configOptions, ...request } = params;
const initial = initialConfigOptions(configOptions);
while (this.providerUpdate) await this.providerUpdate;
const response = await this.createSession(params, {
const created = await this.createSession(request, {
// Revisit these meta values once we support resume
resume: (params._meta as NewSessionMeta | undefined)?.claudeCode?.options?.resume,
});
const response = await this.withInitialConfigOptions(created, initial);
this.afterSetupResponse(response.sessionId, params.mcpServers);
return response;
}
/**
* `session/options`: the modes and config options that `session/new` with
* the same `cwd`, additional directories and `_meta` would answer with, or
* `session/load` and `session/resume` of `sessionId`.
*
* A session that this connection holds answers with what it has now.
* Otherwise a query starts as the session's would, without MCP servers, and
* closes once its options are read (`optionsOnly` in `createSession`). The
* query does not persist: it stores no session, and it does not write to
* the transcript of `sessionId`. Claude Code runs the user's `SessionStart`
* hooks in it, as in every query.
*
* It sends no update, and it creates, loads and changes no session. A
* `sessionId` without a stored conversation fails with `-32002`.
*/
async sessionOptions(params: SessionOptionsRequest): Promise<SessionOptionsResponse> {
while (this.providerUpdate) await this.providerUpdate;
const held = params.sessionId != null ? this.sessions[params.sessionId] : undefined;
if (held) {
await this.validateCwd(params.cwd, params.additionalDirectories);
return { modes: held.modes, configOptions: held.configOptions };
}
const { modes, configOptions } = await this.createSession(
{
cwd: params.cwd,
mcpServers: [],
additionalDirectories: params.additionalDirectories,
_meta: params._meta,
},
{ resume: params.sessionId ?? undefined, optionsOnly: true },
);
return { modes, configOptions };
}
/**
* The setup response once the request's `configOptions` are set on the
* session, in list order, as `session/set_config_option` sets each one. An
* entry that the session refuses is skipped (`applyInitialConfigOptions`).
* No update goes out for them: the response carries the modes and options.
*/
private async withInitialConfigOptions<R extends NewSessionResponse>(
response: R,
entries: readonly InitialSessionConfigOption[],
): Promise<R> {
if (entries.length === 0) return response;
const { sessionId } = response;
await this.serializeConfigurationChange(sessionId, () =>
applyInitialConfigOptions(
entries,
(entry) => this.selectSessionConfigOption({ ...entry, sessionId }, true),
this.logger,
),
);
const session = this.sessions[sessionId];
if (!session) return response;
return { ...response, modes: session.modes, configOptions: session.configOptions };
}
/**
* Start the CLI of a loaded session, and replay the history while the CLI
* starts. The CLI start takes most of the load time, and the replay needs
@@ -2219,8 +2064,12 @@ export class ClaudeAcpAgent {
}
async resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
const initial = initialConfigOptions(params.configOptions);
while (this.providerUpdate) await this.providerUpdate;
const result = await this.getOrCreateSession(params);
const result = await this.withInitialConfigOptions(
await this.getOrCreateSession(params),
initial,
);
this.afterSetupResponse(params.sessionId, params.mcpServers ?? []);
return result;
@@ -2228,6 +2077,7 @@ export class ClaudeAcpAgent {
async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
const timing = new SessionTiming(this.logger, "load", params.sessionId);
const initial = initialConfigOptions(params.configOptions);
while (this.providerUpdate) await this.providerUpdate;
let result: NewSessionResponse;
if (this.sessions[params.sessionId]) {
@@ -2239,6 +2089,7 @@ export class ClaudeAcpAgent {
result = await this.createSessionWhileReplaying(params, timing);
}
timing.phase("replay");
result = await this.withInitialConfigOptions(result, initial);
// After the replay, so the commands do not interleave with the history.
this.afterSetupResponse(params.sessionId, params.mcpServers ?? []);
@@ -3522,12 +3373,17 @@ export class ClaudeAcpAgent {
kind: ClaudeFailureKind,
options: {
turnScoped?: boolean;
/** The turn the failure belongs to, when it is not `activeTurn`. */
turnId?: string;
title?: string;
details?: string;
severity?: "warning" | "error";
} = {},
) => {
const turnId = options.turnScoped === false ? undefined : session.activeTurn?.promptUuid;
const turnId =
options.turnScoped === false
? undefined
: (options.turnId ?? session.activeTurn?.promptUuid);
await sessionFailures.publish(kind, {
turnId,
sessionScoped: options.turnScoped === false,
@@ -5506,19 +5362,20 @@ export class ClaudeAcpAgent {
// here would outlive the `auth_required` rejection with no
// action to clear it (issue #1072).
if (kind === "auth_required") break;
// `no_response` (SDK 0.3.261+): the API sent no response headers
// within the first-byte window, so this retry waits longer for
// them. Say so — "attempt 1 of 1" alone reads like a final
// failure, and the wait is what the user is about to sit through.
const seconds = (ms: number) => `${Math.max(1, Math.round(ms / 1000))}s`;
const noResponse = message.no_response
? ` No response after ${seconds(message.no_response.waited_ms)}; waiting up to ${seconds(message.no_response.retry_wait_ms)}.`
: "";
const title =
message.error_status === null
? `Reconnecting to Claude, attempt ${message.attempt} of ${message.max_retries}.${noResponse}`
: `Retrying Claude, attempt ${message.attempt} of ${message.max_retries}.${noResponse}`;
await publishSessionFailure(kind, { title, severity: "warning" });
// The CLI holds the echo of a prompt back until the first
// assistant frame or stream event of the turn, so the retries
// of a request that gets no response come before the echo
// activates the turn. Give them the queue head, the prompt
// being sent: all retries and the terminal failure of one turn
// then share its `${turnId}:error` row.
const retryTurn =
session.activeTurn ??
(asyncTasks.autonomousTurnOpen ? undefined : firstUnsettledQueuedTurn());
await publishSessionFailure(kind, {
turnId: retryTurn?.promptUuid,
...apiRetryWarning(message),
severity: "warning",
});
break;
}
case "model_refusal_fallback": {
@@ -7876,6 +7733,11 @@ export class ClaudeAcpAgent {
return savedSessionStatus(sessionId);
}
/** See {@link sessionIndex.SessionIndexConnection.setTitle}. */
setSessionTitle(params: SetSessionTitleRequest): Promise<SetSessionTitleResponse> {
return this.sessionIndex.setTitle(params);
}
/** Per client: see {@link sessionIndex.SessionIndexConnection.deleteSession}. */
async deleteSession(params: DeleteSessionRequest): Promise<DeleteSessionResponse> {
return this.sessionIndex.deleteSession(params);
@@ -7895,8 +7757,14 @@ export class ClaudeAcpAgent {
);
}
/**
* `duringSetup`: the value is one of a setup request's `configOptions`. No
* mode update goes out, and the warning of an Auto mode fallback waits for
* the next prompt.
*/
private async selectSessionConfigOption(
params: SetSessionConfigOptionRequest,
duringSetup = false,
): Promise<SetSessionConfigOptionResponse> {
const session = this.sessions[params.sessionId];
if (!session) {
@@ -7927,8 +7795,12 @@ export class ClaudeAcpAgent {
let effectiveValue = resolvedValue;
if (params.configId === MODE_CONFIG_ID) {
effectiveValue = await this.sessionModes.selectMode(params.sessionId, resolvedValue);
await this.sessionModes.publishCurrent(params.sessionId, effectiveValue);
effectiveValue = await this.sessionModes.selectMode(
params.sessionId,
resolvedValue,
duringSetup,
);
if (!duringSetup) await this.sessionModes.publishCurrent(params.sessionId, effectiveValue);
} else if (params.configId === MODEL_CONFIG_ID) {
await this.sessions[params.sessionId].query.setModel(resolvedValue);
}
@@ -7936,7 +7808,13 @@ export class ClaudeAcpAgent {
// effort changes and effort changes induced by a model switch go through
// the same path.
await this.applyConfigOptionValue(params.sessionId, session, params.configId, effectiveValue);
await this.applyConfigOptionValue(
params.sessionId,
session,
params.configId,
effectiveValue,
duringSetup,
);
return { configOptions: session.configOptions };
}
@@ -7948,7 +7826,12 @@ export class ClaudeAcpAgent {
): Promise<void> {
const replayStartedAt = performance.now();
const toolUseCache: ToolUseCache = {};
const messages = resumedMessages ?? (await getSessionMessages(sessionId));
// A load replays the messages before each compaction too, unless reading them fails.
const messages = await readHistoryAcrossCompactions(
sessionId,
resumedMessages ?? (await getSessionMessages(sessionId)),
this.logger,
);
const historyLoadedAt = performance.now();
this.logger.log(
`[session/replay] sessionId=${sessionId} phase=read durationMs=${Math.round(historyLoadedAt - replayStartedAt)} messages=${messages.length}`,
@@ -9447,6 +9330,8 @@ export class ClaudeAcpAgent {
session: Session,
configId: string,
value: string,
/** See {@link selectSessionConfigOption}. */
duringSetup = false,
): Promise<void> {
if (configId === MODE_CONFIG_ID) {
this.sessionModes.syncConfig(session, value);
@@ -9556,7 +9441,8 @@ export class ClaudeAcpAgent {
// still precedes the caller's config_option_update so order-sensitive
// clients update currentModeId before re-rendering the option list.
if (modeDowngraded) {
await this.sessionModes.publishFallbackState(sessionId, session);
if (duringSetup) this.sessionModes.deferFallbackWarning(session);
else await this.sessionModes.publishFallbackState(sessionId, session);
}
// Last, so the switch's own control requests don't queue behind it.
if (modelChanged) this.refreshContextWindowInBackground(sessionId, session);
@@ -9855,6 +9741,12 @@ export class ClaudeAcpAgent {
replayState?: ReplayState;
/** A close of this session during startup cancels registration of its replacement. */
replacingSession?: Session;
/**
* `session/options`: the query answers with the modes and config options
* and closes. No session is registered, and Claude Code stores nothing
* of the query (`persistSession: false`).
*/
optionsOnly?: boolean;
} = {},
): Promise<NewSessionResponse> {
const createStartedAt = performance.now();
@@ -10247,6 +10139,7 @@ export class ClaudeAcpAgent {
},
...(creationOpts.resume !== undefined && { resume: creationOpts.resume }),
...(creationOpts.forkSession !== undefined && { forkSession: creationOpts.forkSession }),
...(creationOpts.optionsOnly ? { persistSession: false } : {}),
abortController,
};
@@ -10330,15 +10223,15 @@ export class ClaudeAcpAgent {
}
}
// Apply user's `availableModels` allowlist from settings.json before any
// Apply the effective `availableModels` allowlist before any
// downstream model handling. `initializationResult.models` is already
// policy-filtered by the CLI; we rebuild the picker from the allowlist
// so the user's exact spellings (and `modelOverrides` targets) are the
// values passed to `setModel`, keeping configOptions, the current-model
// resolver, and the stored modelInfos consistent with what the user
// configured. Managed `deniedModels` drops entries the CLI would refuse.
const settingsAvailableModels = settingsManager.getSettings().availableModels;
const settingsModelOverrides = settingsManager.getSettings().modelOverrides;
const { availableModels: settingsAvailableModels, modelOverrides: settingsModelOverrides } =
settingsManager.getModelSelectionSettings(configuredSettingsObject);
const resumedModel = await resumedModelHint;
// A resumed Fable 5.2 session lists Fable 5.2 as `fable`, as a new
// session does (see `aliasResumedModelRow`).
@@ -10418,7 +10311,8 @@ export class ClaudeAcpAgent {
// `fast_mode_disabled_reason` reflects the post-switch model since SDK
// 0.3.219 (the initialize response used to answer from the spawn-time
// model). A fresh SDK session reports `sdk_opt_in_required` — the toggle IS
// the opt-in — which normalizes away, so only real blockers are retained.
// the opt-in — which normalizes to `undefined`, as does `pending`; every
// other reason is retained.
const fastModeDisabledReason = fastModeEnabled
? undefined
: normalizeFastModeDisabledReason(initializationResult.fast_mode_disabled_reason);
@@ -10466,6 +10360,10 @@ export class ClaudeAcpAgent {
},
ultracode,
);
if (creationOpts.optionsOnly) {
this.discardUnregisteredQuery(q, input, settingsManager);
return { sessionId, modes, configOptions };
}
const initialEffort = configOptions.find((option) => option.id === EFFORT_CONFIG_ID);
if (useRecommendedValue && typeof initialEffort?.currentValue === "string") {
await q.applyFlagSettings(effortFlagSettings(initialEffort.currentValue, ultracodeEnabled));
@@ -10584,7 +10482,8 @@ export class ClaudeAcpAgent {
this.claudeIsWorking(sessionId, registered) ||
(registered.openUserInputs?.size ?? 0) > 0,
contextTokens: () => registered.contextUsedTokens,
settings: () => registered.settingsManager.getSettings(),
settings: () =>
registered.settingsManager.getCompactionSettings(configuredSettingsObject),
compact: () =>
this.prompt(
{ sessionId, prompt: [{ type: "text", text: "/compact" }] },
@@ -11149,9 +11048,11 @@ export function v1AgentApp(
)
.onRequest(methods.agent.session.list, (ctx) => agent.listSessions(ctx.params))
.onRequest(methods.agent.session.delete, (ctx) => agent.deleteSession(ctx.params))
.onRequest(methods.agent.session.setTitle, (ctx) => agent.setSessionTitle(ctx.params))
.onRequest(methods.agent.session.resume, (ctx) => agent.resumeSession(ctx.params))
.onRequest(methods.agent.session.close, (ctx) => agent.closeSession(ctx.params))
.onRequest(methods.agent.session.status, (ctx) => agent.sessionStatus(ctx.params))
.onRequest(methods.agent.session.options, (ctx) => agent.sessionOptions(ctx.params))
.onRequest(methods.agent.session.setMode, (ctx) => agent.setSessionMode(ctx.params))
.onRequest(methods.agent.session.setConfigOption, (ctx) =>
agent.setSessionConfigOption(ctx.params),

35
src/v1/async-task-stop.ts Normal file
View File

@@ -0,0 +1,35 @@
import { RequestError } from "@agentclientprotocol/sdk/unstable";
/** Stops one Claude background task without cancelling the parent prompt turn. */
export const ASYNC_TASK_STOP_METHOD = "_session/async_task/stop";
export type AsyncTaskStopRequest = {
sessionId: string;
asyncTaskId: string;
};
export type AsyncTaskStopResponse = {
stopped: boolean;
};
export function parseAsyncTaskStopRequest(value: unknown): AsyncTaskStopRequest {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw RequestError.invalidParams(undefined, "async task stop params must be an object");
}
const params = value as Record<string, unknown>;
const sessionId = typeof params.sessionId === "string" ? params.sessionId.trim() : "";
const asyncTaskId = typeof params.asyncTaskId === "string" ? params.asyncTaskId.trim() : "";
if (!sessionId) {
throw RequestError.invalidParams(
undefined,
"async task stop params require a non-empty sessionId",
);
}
if (!asyncTaskId) {
throw RequestError.invalidParams(
undefined,
"async task stop params require a non-empty asyncTaskId",
);
}
return { sessionId, asyncTaskId };
}

View File

@@ -0,0 +1,147 @@
import type { Options } from "@anthropic-ai/claude-agent-sdk";
import type { NewSessionRequest } from "@agentclientprotocol/sdk/unstable";
import type { NewSessionMeta } from "../new-session-meta.js";
/** Normalize skills without changing their SDK semantics. Array order and
* duplicates do not affect the selected skill set, so neither should rebuild
* the underlying Query process. */
function normalizeSkills(skills: Options["skills"]): Options["skills"] {
return Array.isArray(skills) ? [...new Set(skills)].sort() : skills;
}
/** Whether a changed `_meta.claudeCode.options` key on session/load or
* session/resume must rebuild the Query process. Almost every option is read
* only when the process starts, so the default is `true`. Every key of the
* SDK's `Options` must be classified: an SDK bump that adds or removes an
* option fails to compile until it is decided here. */
const OPTION_REBUILDS_SESSION = {
additionalDirectories: true,
agentProgressSummaries: true,
agents: true,
allowDangerouslySkipPermissions: true,
allowedTools: true,
betas: true,
debug: true,
debugFile: true,
disallowedTools: true,
effort: true,
enableFileCheckpointing: true,
env: true,
executableArgs: true,
extraArgs: true,
fallbackModel: true,
forwardSubagentText: true,
includeHookEvents: true,
managedSettings: true,
maxBudgetUsd: true,
maxThinkingTokens: true,
maxTurns: true,
mcpServers: true,
model: true,
outputFormat: true,
pathToClaudeCodeExecutable: true,
permissionPrompts: true,
permissionPromptToolName: true,
persistSession: true,
perTaskStopAffordance: true,
planModeInstructions: true,
pluginDelivery: true,
plugins: true,
projectConfigRoot: true,
promptSuggestions: true,
sandbox: true,
sessionStoreFlush: true,
settings: true,
settingSources: true,
skills: true,
strictMcpConfig: true,
supportedDialogKinds: true,
systemPrompt: true,
taskBudget: true,
thinking: true,
toolAliases: true,
toolConfig: true,
tools: true,
verbatimPrompts: true,
// Managed or ignored by the adapter.
agent: false,
cwd: false,
executable: false,
includePartialMessages: false,
permissionMode: false,
// Per-call controls for attaching to the stored conversation.
continue: false,
forkSession: false,
resume: false,
resumeDropsTurn: false,
resumeSessionAt: false,
sessionId: false,
// Callbacks and live objects: they cannot cross JSON-RPC and have no stable serialized form.
abortController: false,
canUseTool: false,
hooks: false,
onElicitation: false,
onUserDialog: false,
sessionStore: false,
spawnClaudeCodeProcess: false,
stderr: false,
// No effect on an already-created session.
loadTimeoutMs: false,
title: false,
} as const satisfies Record<keyof Options, boolean>;
/** A JSON value with object keys sorted, so key order never changes a
* fingerprint. Functions and class instances (hook callbacks, abort
* controllers, in-process MCP servers) cannot cross JSON-RPC and have no
* stable serialized form, so they are left out. */
function canonicalJson(value: unknown): unknown {
if (typeof value === "function") return undefined;
if (value === null || typeof value !== "object") return value;
if (Array.isArray(value)) return value.map(canonicalJson);
const proto = Object.getPrototypeOf(value);
if (proto !== Object.prototype && proto !== null) return undefined;
return Object.fromEntries(
Object.keys(value)
.sort()
.map((key) => [key, canonicalJson((value as Record<string, unknown>)[key])])
.filter(([, entry]) => entry !== undefined),
);
}
/** Compute a stable fingerprint of the session-defining params so we can
* detect when a loadSession/resumeSession call requires tearing down and
* recreating the underlying Query process. Every option classified in
* {@link OPTION_REBUILDS_SESSION} is covered, so a warm resume never silently
* keeps stale values. MCP servers are sorted by name, and skills are
* normalized as a set, so ordering differences don't trigger unnecessary
* recreations. */
export function computeSessionFingerprint(params: {
cwd: string;
mcpServers?: NewSessionRequest["mcpServers"];
additionalDirectories?: NewSessionRequest["additionalDirectories"];
systemPrompt?: NewSessionRequest["systemPrompt"];
_meta?: NewSessionRequest["_meta"];
}): string {
const servers = [...(params.mcpServers ?? [])].sort((a, b) => a.name.localeCompare(b.name));
const meta = params._meta as (NewSessionMeta & Record<string, unknown>) | undefined;
const options: Record<string, unknown> = Object.fromEntries(
Object.entries(meta?.claudeCode?.options ?? {}).filter(
([key]) => (OPTION_REBUILDS_SESSION as Record<string, boolean>)[key] === true,
),
);
options.skills = normalizeSkills(options.skills as Options["skills"]);
return JSON.stringify(
canonicalJson({
cwd: params.cwd,
mcpServers: servers,
additionalDirectories: params.additionalDirectories,
additionalRoots: meta?.additionalRoots,
systemPrompt: meta?.systemPrompt,
clientSystemPrompt: params.systemPrompt ?? undefined,
disableBuiltInTools: meta?.disableBuiltInTools,
emitRawSDKMessages: meta?.claudeCode?.emitRawSDKMessages,
options,
}),
);
}

View File

@@ -15,11 +15,12 @@ import {
type InitializeRequest,
type ListSessionsRequest,
type ListSessionsResponse,
type SetSessionTitleRequest,
type SetSessionTitleResponse,
} from "@agentclientprotocol/sdk/unstable";
import type { Query } from "@anthropic-ai/claude-agent-sdk";
import type { Session } from "../acp-agent.js";
import type { AcpClient } from "../../acp-client.js";
import type { Logger } from "../../logger.js";
import type { Session } from "./acp-agent.js";
import type { AcpClient } from "../acp-client.js";
import type { Logger } from "../logger.js";
import {
AIR_SESSION_ARCHIVE_CAPABILITY,
AIR_SESSION_BATCH_GET_CAPABILITY,
@@ -29,9 +30,10 @@ import {
AIR_SESSION_WORKTREES_CAPABILITY,
clientSupportsAirCapability,
withAirMeta,
} from "../../air-extension.js";
import type { OwnSessionState } from "./activity.js";
import { isArchivedTitle, storedTitle, visibleTitle } from "./archive-title.js";
} from "../air-extension.js";
import type { OwnSessionState } from "../session-index/activity.js";
import { isArchivedTitle, visibleTitle } from "../session-index/archive-title.js";
import { renameSession, retitleSession, type RetitledSession } from "../session-index/rename.js";
import {
LIST_CHANGES_METHOD,
LIST_SUBSCRIBE_METHOD,
@@ -41,12 +43,11 @@ import {
type ListSubscribeRequest,
type ListSubscribeResponse,
type ListUnsubscribeRequest,
} from "./list-subscriptions.js";
} from "../session-index/list-subscriptions.js";
import {
archiveInsteadOfDelete,
deleteSessionIfStored,
archiveTo,
renameTo,
parseRenameSessionRequest,
parseSessionIdRequest,
parseBatchGetSessionsRequest,
@@ -55,13 +56,12 @@ import {
SESSION_RENAME_METHOD,
SESSION_UNARCHIVE_METHOD,
SessionIndexService,
transcriptWriter,
type RenameSessionRequest,
type RetitleOptions,
type SessionIdRequest,
type TitleChange,
type BatchGetSessionsRequest,
type BatchGetSessionsResponse,
} from "./service.js";
} from "../session-index/service.js";
/** What the session index keeps on each `Session`. */
export type SessionIndexFields = {
@@ -115,9 +115,9 @@ export class SessionIndexConnection {
* exactly as before.
*/
service?: SessionIndexService;
/** The archive behind `session/delete` of an AIR client without
* `sessionIndex`: the same writes, without a list or a watcher. */
private archiver?: SessionIndexService;
/** The transcript writes of a client without `sessionIndex` (see
* {@link transcriptWriter}). */
private transcripts?: SessionIndexService;
/** When the query of a session failed here, by session id. */
private readonly failedQueries = new Map<string, number>();
@@ -250,24 +250,34 @@ export class SessionIndexConnection {
return sessions.filter((session) => !isArchivedTitle(session.customTitle));
}
/** `_session/rename`: names a session; no generated title replaces it.
* An archived session stays archived. */
/** `_session/rename` of a `sessionIndex` client (see {@link renameSession}). */
async rename(request: RenameSessionRequest): Promise<EmptyResponse> {
const index = this.requireService(SESSION_RENAME_METHOD);
const { sessionId } = request;
// As the CLI keeps it, and never with the archive prefix: a rename does
// not change the archive state.
const title = storedTitle(request.title, false, sessionId);
// The title as stored: an archived one is cut with its prefix.
const shown = (stored: string | undefined) =>
stored === undefined ? title : visibleTitle(stored);
await this.retitle(index, sessionId, renameTo(title), shown, {
mayBeUnwritten: true,
sidecar: "always",
});
await this.renameWith(this.requireService(SESSION_RENAME_METHOD), request);
return {};
}
/** `session/set_title`, for every client: the rename of `_session/rename`,
* without the session index of a client that did not ask for it. */
async setTitle(request: SetSessionTitleRequest): Promise<SetSessionTitleResponse> {
const title = await this.renameWith(
this.service ?? this.writer(),
parseRenameSessionRequest(request),
);
return { title };
}
private renameWith(index: SessionIndexService, request: RenameSessionRequest): Promise<string> {
return renameSession(index, request, this.loaded(request.sessionId), (stored) =>
this.clientTitle(stored),
);
}
/** The session loaded here, as a rename reaches it. */
private loaded(sessionId: string): RetitledSession | undefined {
const session = this.host.agent.sessions[sessionId];
return session && { session, titles: session.titles };
}
/** `_session/archive`: hides a session from the default list and stops
* it. Idempotent; the session need not be loaded. A session loaded here
* is interrupted, retitled through its CLI while that still runs, then
@@ -279,7 +289,7 @@ export class SessionIndexConnection {
// No turn goes on after the archive: the interrupt goes to the CLI ahead
// of the rename.
if (loaded) await this.host.interruptSession(sessionId);
await this.retitle(index, sessionId, archiveTo(true), undefined, {
await retitleSession(index, sessionId, archiveTo(true), undefined, this.loaded(sessionId), {
mayBeUnwritten: this.isUnwrittenSession(sessionId),
sidecar: "existing",
});
@@ -308,7 +318,7 @@ export class SessionIndexConnection {
async unarchive(params: SessionIdRequest): Promise<EmptyResponse> {
const index = this.requireService(SESSION_UNARCHIVE_METHOD);
const { sessionId } = params;
await this.retitle(index, sessionId, archiveTo(false), undefined, {
await retitleSession(index, sessionId, archiveTo(false), undefined, this.loaded(sessionId), {
mayBeUnwritten: this.isUnwrittenSession(sessionId),
sidecar: "existing",
});
@@ -316,58 +326,6 @@ export class SessionIndexConnection {
return {};
}
/**
* Retitles a session (see {@link SessionIndexService.retitle}). A session
* this connection runs is titled through its CLI (`rename_session`). A
* loaded session's title state takes the change as a client title, so no
* generated title replaces it; `publish` is the title it then publishes.
*/
private async retitle(
index: SessionIndexService,
sessionId: string,
change: TitleChange,
publish: Parameters<Session["titles"]["setExplicitTitle"]>[0],
options: Pick<RetitleOptions, "mayBeUnwritten" | "sidecar">,
): Promise<void> {
const session = this.host.agent.sessions[sessionId];
// Decided once a title generation in flight has ended: the query may
// have closed meanwhile.
const persist = () => {
const query = session?.query as
| (Query & { renameSession?: (title: string, sessionId?: string) => Promise<void> })
| undefined;
// A closed session's CLI is gone here: only a running query titles it.
const running = session !== undefined && !session.queryClosed;
if (running && typeof query?.renameSession !== "function") {
// Its CLI holds the title and would write it back over ours.
throw RequestError.invalidRequest(
{ sessionId },
"The Claude Code CLI of this session cannot change its title while it runs.",
);
}
const live = running
? {
cwd: session.cwd,
rename: (title: string) => query!.renameSession!(title, sessionId),
stored: () => session.titles.storedTitle,
remember: (title: string) => session.titles.rememberStoredTitle(title),
shown: session.titles.shownTitle,
}
: undefined;
return index.retitle(sessionId, change, {
...options,
// Without its CLI, a session needs its transcript.
mayBeUnwritten: live !== undefined && options.mayBeUnwritten,
live,
});
};
if (session) {
await session.titles.setExplicitTitle(publish, persist);
} else {
await persist();
}
}
/**
* `session/delete`:
* - A `sessionIndex` client deletes for real: every transcript, whoever
@@ -388,19 +346,19 @@ export class SessionIndexConnection {
if (this.service) {
await this.service.delete(params.sessionId, loaded);
} else if (this.host.isAirClient()) {
await archiveInsteadOfDelete(params.sessionId, this.airArchiver());
await archiveInsteadOfDelete(params.sessionId, this.writer());
} else {
await deleteSessionIfStored(params.sessionId);
// After a `session/set_title` of the session in flight.
await this.writer().exclusive(params.sessionId, () =>
deleteSessionIfStored(params.sessionId),
);
}
return {};
}
private airArchiver(): SessionIndexService {
this.archiver ??= new SessionIndexService({
logError: (message, error) =>
this.host.agent.logger.error(`[session-index] ${message}:`, error),
});
return this.archiver;
private writer(): SessionIndexService {
this.transcripts ??= transcriptWriter(this.host.agent.logger);
return this.transcripts;
}
private requireService(method: string): SessionIndexService {

73
src/v1/steering.ts Normal file
View File

@@ -0,0 +1,73 @@
import { PromptRequest, RequestError } from "@agentclientprotocol/sdk/unstable";
/** Custom (extension) request method a client uses to steer the turn that is
* currently running: the message is injected into the in-flight turn rather
* than queued as a separate `session/prompt`. Named `_session/steering` per the
* agreed ACP steering wire protocol; advertised to clients via the top-level
* `InitializeResponse._meta.steering.supported`. */
export const STEER_METHOD = "_session/steering";
/** How urgently the SDK delivers a steered message relative to the running
* turn — an internal Claude implementation detail, not part of the wire
* contract. `now` pre-empts the current generation, while `later` waits for a
* pending permission/elicitation callback to settle instead of cancelling its
* ACP request and hiding the client's user-input card (IJAI-1191). */
export const STEER_PRIORITY_NOW = "now" as const;
export const STEER_PRIORITY_LATER = "later" as const;
/** Request-level steering options. `promptRequired` is opt-in so existing Hosts
* keep the established idle fallback behavior. */
type SteerMeta = {
[key: string]: unknown;
steering?: {
idleBehavior?: "promptRequired";
};
};
/** Params of a {@link STEER_METHOD} request. Shaped like the relevant subset of
* a `PromptRequest` so the same `promptToClaude` conversion applies. Delivery
* priority is deliberately NOT exposed here — it's an internal detail the agent
* chooses (see {@link STEER_PRIORITY}). */
export type SteerRequest = {
sessionId: string;
prompt: PromptRequest["prompt"];
_meta?: SteerMeta | null;
};
/** Result of a {@link STEER_METHOD} request. The legacy `startedNewTurn` result
* remains the default idle behavior; `promptRequired` is returned only when the
* Host explicitly opts into the host-owned fallback in request `_meta`. */
export type SteerResponse =
| { outcome: "injected" }
| { outcome: "startedNewTurn" }
| { outcome: "promptRequired"; reason: "noRunningTurn" };
/** Validate raw JSON-RPC params into a {@link SteerRequest}. Kept minimal — the
* content blocks are handed to `promptToClaude`, which tolerates unknown block
* types — but `sessionId` and a non-empty `prompt` array are required. */
export function parseSteerRequest(params: unknown): SteerRequest {
if (!params || typeof params !== "object") {
throw RequestError.invalidParams(undefined, "steer params must be an object");
}
const { sessionId, prompt, _meta } = params as Record<string, unknown>;
if (typeof sessionId !== "string" || sessionId.length === 0) {
throw RequestError.invalidParams(undefined, "steer params require a non-empty sessionId");
}
if (!Array.isArray(prompt) || prompt.length === 0) {
throw RequestError.invalidParams(undefined, "steer params require a non-empty prompt array");
}
const steering =
_meta && typeof _meta === "object" ? (_meta as Record<string, unknown>).steering : undefined;
const idleBehavior =
steering && typeof steering === "object"
? (steering as Record<string, unknown>).idleBehavior
: undefined;
if (idleBehavior !== undefined && idleBehavior !== "promptRequired") {
throw RequestError.invalidParams(undefined, "unsupported steering idleBehavior");
}
return {
sessionId,
prompt: prompt as PromptRequest["prompt"],
_meta: _meta as SteerMeta | null | undefined,
};
}

View File

@@ -39,17 +39,32 @@ import { forkSession } from "../fork-session.js";
import { supportsMcpOAuth } from "../mcp-oauth.js";
import { sdkMcpServers } from "../mcp-servers.js";
import { ALLOW_BYPASS, autoModeDisabled } from "../permissions/modes.js";
import { readResumedTail, resumedModelFromTranscript } from "../resumed-session.js";
import {
readHistoryAcrossCompactions,
readResumedTail,
resumedModelFromTranscript,
} from "../resumed-session.js";
import { mergeEffortSettings } from "../session-effort.js";
import { configuredSettings, resolveThinkingConfig } from "../session-env.js";
import { resolveInitialPermissionMode } from "../session-mode.js";
import { sanitizeTitle } from "../session-titles.js";
import { renameSession } from "../session-index/rename.js";
import {
parseRenameSessionRequest,
transcriptWriter,
type SessionIndexService,
} from "../session-index/service.js";
import { SettingsManager } from "../settings.js";
import { STARTUP_FAILURE_RESULTS_ENV, startupFailureError } from "../startup-failure.js";
import { createPostToolUseHook } from "../tools.js";
import { Pushable } from "../utils.js";
import { V2_SESSION_CAPABILITIES } from "./initialize.js";
import { v1ForkSessionRequest, v1NewSessionRequest } from "./session-requests.js";
import {
initialConfigRequests,
v1ForkSessionRequest,
v1NewSessionRequest,
} from "./session-requests.js";
import { SessionCommands } from "./commands.js";
import { V2UpdateTranslator } from "./update-translator.js";
import { airInitializeMeta, airSupport, NO_AIR, type AirSupport } from "./air.js";
import { V2Auth } from "./auth.js";
@@ -108,7 +123,9 @@ export function v2AgentApp(
.onRequest(v2.methods.agent.session.list, ({ params }) => agent.listSessions(params))
.onRequest(v2.methods.agent.session.close, ({ params }) => agent.closeSession(params))
.onRequest(v2.methods.agent.session.status, ({ params }) => agent.sessionStatus(params))
.onRequest(v2.methods.agent.session.options, ({ params }) => agent.sessionOptions(params))
.onRequest(v2.methods.agent.session.delete, ({ params }) => agent.deleteSession(params))
.onRequest(v2.methods.agent.session.setTitle, ({ params }) => agent.setTitle(params))
.onRequest(v2.methods.agent.session.setConfigOption, ({ params }) =>
agent.setConfigOption(params),
)
@@ -154,6 +171,8 @@ class V2Agent implements AgentHandle, SessionConnection {
private readonly guard: SubscriptionGuard;
/** The restarts of the sessions' queries, for a new route or account (see `restarts.ts`). */
private readonly restarts: SessionRestarts;
/** The transcript writes of `session/set_title`, made at its first call (see `transcriptWriter`). */
private transcripts: SessionIndexService | undefined;
/** What the client declared in `initialize`. */
private clientCapabilities: v2.ClientCapabilities | null | undefined;
/** The elicitation modes that the client advertised in `initialize`. */
@@ -308,7 +327,9 @@ class V2Agent implements AgentHandle, SessionConnection {
async newSession(params: v2.NewSessionRequest): Promise<v2.NewSessionResponse> {
const sessionId = randomUUID();
const initial = initialConfigRequests(sessionId, params.configOptions);
const session = await this.startSession(sessionId, params, undefined);
await session.setInitialConfigOptions(initial);
const response = { sessionId, ...(await this.setupFields(session)) };
this.afterSetup(session, params);
return response;
@@ -335,7 +356,9 @@ class V2Agent implements AgentHandle, SessionConnection {
/**
* Resumes a stored session, or attaches to a live one. With `replayFrom:
* { type: "start" }` the stored history replays first. Either way, the
* state snapshot goes out after any replay and before the response.
* state snapshot goes out after any replay and before the response. The
* request's `configOptions` are set before the replay, on an active session
* too.
*/
resumeSession(params: v2.ResumeSessionRequest): Promise<v2.ResumeSessionResponse> {
const { sessionId } = params;
@@ -361,23 +384,21 @@ class V2Agent implements AgentHandle, SessionConnection {
);
}
if (this.ownerOfChild(sessionId)) throw restrictedChild(sessionId, "resume");
const initial = initialConfigRequests(sessionId, params.configOptions);
await this.restarts.settled(sessionId);
// A concurrent resume of this session started its query: this one then
// attaches to the session that it started.
await this.startSettled(sessionId);
let session = this.sessions.get(sessionId);
let history: Promise<SessionMessage[]> | undefined;
// The replay: the messages before each compaction too, unless reading them fails.
let replayed: Promise<SessionMessage[]> | undefined;
if (session) {
// Resuming the active session synchronizes it again. Its work goes on,
// and the request's MCP servers and additional directories replace the
// session's.
const request = await validatedSetup(params);
if (request.cwd !== session.cwd) {
throw RequestError.invalidParams(
{ cwd: request.cwd },
`Session ${sessionId} works in ${session.cwd}: resume it with that \`cwd\``,
);
}
assertCwdOf(session, request.cwd, "resume it");
await session.setMcpServers({
...clientOptions(params._meta)?.mcpServers,
...sdkMcpServers(request.mcpServers),
@@ -393,13 +414,23 @@ class V2Agent implements AgentHandle, SessionConnection {
} else {
// One read of the transcript serves the replay and the resumed model.
history = replayFrom != null ? getSessionMessages(sessionId, { dir: params.cwd }) : undefined;
// A failed start leaves it unawaited.
// The read across compactions overlaps the start of the query.
replayed = history?.then((messages) =>
readHistoryAcrossCompactions(sessionId, messages, this.logger, params.cwd),
);
// A failed start leaves them unawaited.
history?.catch(() => {});
replayed?.catch(() => {});
session = await this.startSession(sessionId, params, sessionId, history);
}
await session.setInitialConfigOptions(initial);
let subagents: SubagentReplay | undefined;
if (replayFrom != null) {
const messages = await (history ?? getSessionMessages(sessionId, { dir: session.cwd }));
const cwd = session.cwd;
const messages = await (replayed ??
getSessionMessages(sessionId, { dir: cwd }).then((chain) =>
readHistoryAcrossCompactions(sessionId, chain, this.logger, cwd),
));
subagents = await session.tasks.loadReplay(messages);
session.output.replay(
messages,
@@ -473,6 +504,66 @@ class V2Agent implements AgentHandle, SessionConnection {
return savedSessionStatus(sessionId);
}
/**
* `session/options`: the config options and commands that `session/new`
* with the same `cwd`, additional directories and `_meta` would answer
* with, or `session/resume` of `sessionId`.
*
* An active session answers with what it has now. Otherwise a query starts
* as the session's would, without MCP servers, and closes once its options
* are read. So the commands lack the MCP prompts, and a failed read of the
* commands leaves them out. The query does not persist
* (`persistSession: false`): it stores no session, and it does not write to
* the transcript of `sessionId`. Claude Code runs the user's `SessionStart`
* hooks in it, as in every query.
*
* It sends no update, and it creates, resumes and changes no session. A
* `sessionId` without a stored conversation fails with `-32002`.
*/
async sessionOptions(params: v2.SessionOptionsRequest): Promise<v2.SessionOptionsResponse> {
const { sessionId } = params;
const setup: v2.NewSessionRequest = {
cwd: params.cwd,
additionalDirectories: params.additionalDirectories,
_meta: params._meta,
};
let session: V2Session | undefined;
if (sessionId != null) {
if (this.ownerOfChild(sessionId)) throw restrictedChild(sessionId, "read the options of");
// A restart or a resume that runs makes the session active.
await this.restarts.settled(sessionId);
await this.startSettled(sessionId);
session = this.sessions.get(sessionId);
}
const fields = (config: SessionConfig, commands: v2.AvailableCommand[]) => ({
configOptions: config.options(),
// The draft leaves out an empty list.
...(commands.length > 0 ? { availableCommands: commands } : {}),
});
const listed = (commands: SessionCommands) =>
commands.list().catch((error: unknown) => {
this.logger.error("session/options: failed to read the commands", error);
return [];
});
if (session) {
assertCwdOf(session, (await validatedSetup(setup)).cwd, "ask for its options");
return fields(session.config, await listed(session.commands));
}
// The probe's own id: its records never meet those of the session `sessionId`.
const probeId = randomUUID();
const probe = await this.openQuery(probeId, setup, sessionId ?? undefined, undefined, {
persist: false,
});
try {
return fields(probe.config, await listed(new SessionCommands(probe.query)));
} finally {
probe.query.close();
probe.settings.dispose();
this.auth.sessionEnded(probeId);
this.guard.sessionEnded(probeId);
}
}
/** Closing is idempotent: a session that is not active closes at once. */
async closeSession({ sessionId }: v2.CloseSessionRequest): Promise<v2.CloseSessionResponse> {
if (this.ownerOfChild(sessionId)) throw restrictedChild(sessionId, "close");
@@ -502,7 +593,9 @@ class V2Agent implements AgentHandle, SessionConnection {
const live = this.sessions.get(sessionId);
if (live || this.stopped.has(sessionId)) await this.closeSession({ sessionId });
try {
await deleteSession(sessionId, live ? { dir: live.cwd } : undefined);
// After a `session/set_title` of the session in flight.
const remove = () => deleteSession(sessionId, live ? { dir: live.cwd } : undefined);
await (this.transcripts ? this.transcripts.exclusive(sessionId, remove) : remove());
} catch (error) {
// A session without a transcript, such as one never prompted, has nothing to delete.
if (error instanceof Error && /not found/i.test(error.message)) return {};
@@ -511,6 +604,30 @@ class V2Agent implements AgentHandle, SessionConnection {
return {};
}
/**
* `session/set_title`, as the v1 core does it (see `session-index/rename.ts`).
* The session need not be active. An active one publishes the title, and no
* generated title replaces it.
*/
async setTitle(params: v2.SetSessionTitleRequest): Promise<v2.SetSessionTitleResponse> {
const request = parseRenameSessionRequest(params);
const { sessionId } = request;
if (this.ownerOfChild(sessionId)) throw restrictedChild(sessionId, "retitle");
// Renamed through the query that runs once a restart or resume settled.
await this.restarts.settled(sessionId);
await this.setupSettled(sessionId);
const writer = (this.transcripts ??= transcriptWriter(this.logger));
for (;;) {
const session = this.sessions.get(sessionId);
const title = await renameSession(writer, request, session?.reports.retitled());
// A restart or resume started another query of the session while the
// rename ran: its CLI may hold the old title and write it back, so it
// is renamed too. A session that only closed took the title already.
const now = this.sessions.get(sessionId);
if (now === undefined || now === session) return { title };
}
}
async setConfigOption(
params: v2.SetSessionConfigOptionRequest,
): Promise<v2.SetSessionConfigOptionResponse> {
@@ -683,10 +800,7 @@ class V2Agent implements AgentHandle, SessionConnection {
return start;
}
/**
* The query start of {@link startSession}. Its options are the client's
* `_meta.claudeCode.options` under the ones this core controls.
*/
/** The query start of {@link startSession}: {@link openQuery}, then the session is active. */
private async launchSession(
sessionId: string,
params: v2.NewSessionRequest | v2.ResumeSessionRequest | v2.ForkSessionRequest,
@@ -694,6 +808,50 @@ class V2Agent implements AgentHandle, SessionConnection {
/** The resumed transcript, when the caller reads it anyway (a replay). */
history?: Promise<SessionMessage[]>,
): Promise<V2Session> {
const {
session,
query: q,
config,
models,
...opened
} = await this.openQuery(sessionId, params, resume, history);
session.reports.configure({
settings: opened.settings,
configuredSettings: opened.configured,
env: opened.env,
mcpServers: opened.request.mcpServers.map((server) => server.name),
mcpOAuth: this.elicitation.url && supportsMcpOAuth(q),
query: q,
model: { id: models.models.currentModelId, info: models.allowlistedModelInfo },
});
session.start(q, config, opened.mcpServers, resume !== undefined);
this.sessions.set(sessionId, session);
this.stopped.delete(sessionId);
// A route change during the start did not find this session live.
this.restarts.track(sessionId, params, {
staleRoute: this.auth.routeGeneration !== opened.routeGeneration,
});
return session;
}
/**
* Starts the query of a session and builds its config. Its options are the
* client's `_meta.claudeCode.options` under the ones this core controls.
* The session is not active yet, and nothing of it goes out. The caller
* closes the query and disposes the settings when it does not keep them.
*
* `resume`: the stored session that the query resumes, `sessionId` itself
* but for a `session/options` probe. Without `persist`, Claude Code stores
* nothing of the query (see {@link sessionOptions}).
*/
private async openQuery(
sessionId: string,
params: v2.NewSessionRequest | v2.ResumeSessionRequest | v2.ForkSessionRequest,
resume: string | undefined,
/** The resumed transcript, when the caller reads it anyway (a replay). */
history?: Promise<SessionMessage[]>,
{ persist = true } = {},
) {
const request = await validatedSetup(params);
const client = clientOptions(params._meta);
// After a provider change that is still listing its models.
@@ -842,6 +1000,7 @@ class V2Agent implements AgentHandle, SessionConnection {
...STARTUP_FAILURE_RESULTS_ENV,
},
...(resume !== undefined ? { resume } : { sessionId }),
...(persist ? {} : { persistSession: false }),
};
const q = query({ prompt: input, options });
let initialization;
@@ -855,7 +1014,7 @@ class V2Agent implements AgentHandle, SessionConnection {
error instanceof Error &&
error.message.includes("No conversation found with session ID")
) {
throw RequestError.sessionNotFound(sessionId);
throw RequestError.sessionNotFound(resume);
}
throw startupFailureError(error) ?? error;
}
@@ -880,6 +1039,7 @@ class V2Agent implements AgentHandle, SessionConnection {
resumed,
sessionId,
this.logger,
configured,
);
} catch (error) {
// Claude Code refused a fresh session's model pin.
@@ -916,22 +1076,18 @@ class V2Agent implements AgentHandle, SessionConnection {
settings.dispose();
throw error;
}
session.reports.configure({
settings,
env: options.env ?? {},
mcpServers: request.mcpServers.map((server) => server.name),
mcpOAuth: this.elicitation.url && supportsMcpOAuth(q),
return {
session,
query: q,
model: { id: models.models.currentModelId, info: models.allowlistedModelInfo },
});
session.start(q, config, mcpServers, resume !== undefined);
this.sessions.set(sessionId, session);
this.stopped.delete(sessionId);
// A route change during the start did not find this session live.
this.restarts.track(sessionId, params, {
staleRoute: this.auth.routeGeneration !== routeGeneration,
});
return session;
config,
models,
settings,
configured,
env: options.env ?? {},
request,
mcpServers,
routeGeneration,
};
}
}
@@ -966,6 +1122,15 @@ async function validatedSetup(
return request;
}
/** A request of the active `session` names its `cwd`, as `session/resume` must. */
function assertCwdOf(session: V2Session, cwd: string, action: string): void {
if (cwd === session.cwd) return;
throw RequestError.invalidParams(
{ cwd },
`Session ${session.sessionId} works in ${session.cwd}: ${action} with that \`cwd\``,
);
}
function sameDirectories(left: readonly string[], right: readonly string[]): boolean {
const normalize = (directories: readonly string[]) =>
[...new Set(directories.map((directory) => path.resolve(directory)))].sort().join("\n");

View File

@@ -43,6 +43,7 @@ import type {
Query,
Settings,
} from "@anthropic-ai/claude-agent-sdk";
import { applyInitialConfigOptions } from "../initial-config-options.js";
import type { Logger } from "../logger.js";
import type { SubagentModels } from "../native-subagents.js";
import { DEFAULT_AGENT_ID } from "../session-config-ids.js";
@@ -346,6 +347,19 @@ export class SessionConfig {
return this.serialized(() => this.apply(params));
}
/**
* Sets the initial values of a setup request's `configOptions`, in list
* order, as {@link set} sets each one. An entry that the session refuses is
* skipped (`applyInitialConfigOptions`). Nothing goes out: the setup
* response carries the options, and the notice of an Auto mode fallback
* waits for the next prompt.
*/
setInitial(entries: readonly v2.SetSessionConfigOptionRequest[]): Promise<void> {
return this.serialized(() =>
applyInitialConfigOptions(entries, (entry) => this.apply(entry, true), this.logger),
);
}
/**
* Sets the choices of `previous`, the config of the query that this one
* replaced (see `restarts.ts`), as the v1 core's `restoreSessionChoices`
@@ -411,7 +425,11 @@ export class SessionConfig {
return result;
}
private async apply(params: v2.SetSessionConfigOptionRequest): Promise<void> {
/** `duringSetup`: see {@link setInitial}. */
private async apply(
params: v2.SetSessionConfigOptionRequest,
duringSetup = false,
): Promise<void> {
const request = v1SetSessionConfigOptionRequest(params);
const option = this.session.configOptions.find((entry) => entry.id === request.configId);
if (!option) {
@@ -429,11 +447,11 @@ export class SessionConfig {
const value = selected.value;
switch (request.configId) {
case MODE_CONFIG_ID:
await this.applyMode(value as PermissionMode);
await this.applyMode(value as PermissionMode, duringSetup);
return;
case MODEL_CONFIG_ID:
await this.session.query.setModel(value);
await this.recordModel(value);
await this.recordModel(value, duringSetup);
return;
case EFFORT_CONFIG_ID:
// Applied first, so a rejected control request leaves the option as it was.
@@ -472,11 +490,17 @@ export class SessionConfig {
return this.rules.effectiveMode(this.session, requested);
}
private async applyMode(requested: PermissionMode): Promise<void> {
private async applyMode(requested: PermissionMode, duringSetup = false): Promise<void> {
const mode = this.rules.effectiveMode(this.session, requested);
await this.session.query.setPermissionMode(mode);
this.rules.syncConfig(this.session, mode);
if (mode !== requested) await this.rules.publishFallbackWarning(this.sessionId, this.session);
if (mode !== requested) await this.warnOfFallback(duringSetup);
}
/** The notice of an Auto mode fallback: now, or with the next prompt during setup. */
private async warnOfFallback(duringSetup: boolean): Promise<void> {
if (duringSetup) this.rules.deferFallbackWarning(this.session);
else await this.rules.publishFallbackWarning(this.sessionId, this.session);
}
/** Claude Code switched the mode itself. Returns whether the option changed. */
@@ -606,7 +630,7 @@ export class SessionConfig {
* show only on a model that supports them, with the user's retained
* choice.
*/
private async recordModel(value: string): Promise<void> {
private async recordModel(value: string, duringSetup = false): Promise<void> {
const info = this.modelInfos.find((model) => model.value === value);
this.session.models = { ...this.session.models, currentModelId: value };
const modeDowngraded = await this.rules.reconcileForModel(this.session, info);
@@ -664,7 +688,7 @@ export class SessionConfig {
effort.error,
);
}
if (modeDowngraded) await this.rules.publishFallbackWarning(this.sessionId, this.session);
if (modeDowngraded) await this.warnOfFallback(duringSetup);
}
}

Some files were not shown because too many files have changed in this diff Show More