Compare commits

...

26 Commits

Author SHA1 Message Date
acp-release-bot[bot]
c5c120dba9 chore(main): release 0.107.0 (#1595)
🤖 I have created a release *beep* *boop*
---


##
[0.107.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.106.0...v0.107.0)
(2026-11-13)


### Features

* report the turn's rate limit in rate-limit failure errors
([#1610](https://github.com/agentclientprotocol/claude-agent-acp/issues/1610))
([4f474d6](4f474d655b))
* report unknown subagent state at a conversation reset and reference
children from tool calls
([#1608](https://github.com/agentclientprotocol/claude-agent-acp/issues/1608))
([4804917](4804917a6f))
* say the cause of an api retry in the AIR warning details
([#1594](https://github.com/agentclientprotocol/claude-agent-acp/issues/1594))
([4f690e1](4f690e129c))


### Bug Fixes

* assume a 1M context window until Claude Code reports it
([#1613](https://github.com/agentclientprotocol/claude-agent-acp/issues/1613))
([9619805](9619805306))
* end the tool calls of a response that Claude Code took back
([#1616](https://github.com/agentclientprotocol/claude-agent-acp/issues/1616))
([21f3e04](21f3e0430b))
* explain why Claude Code turns Fast mode off
([#1602](https://github.com/agentclientprotocol/claude-agent-acp/issues/1602))
([c331e17](c331e178a5))
* follow Claude Code's rule for managed deniedModels in the model picker
([#1615](https://github.com/agentclientprotocol/claude-agent-acp/issues/1615))
([c78bcc7](c78bcc7f41))
* log a managed policy source that did not load
([#1614](https://github.com/agentclientprotocol/claude-agent-acp/issues/1614))
([ca46b41](ca46b412f5))
* report a mid-turn API error notice where the response broke off
([#1599](https://github.com/agentclientprotocol/claude-agent-acp/issues/1599))
([4df6999](4df69996ca))
* say why the login failed in the v2 authRequired error
([#1598](https://github.com/agentclientprotocol/claude-agent-acp/issues/1598))
([ecd066c](ecd066c870))
* update to @anthropic-ai/claude-agent-sdk 0.3.323
([#1612](https://github.com/agentclientprotocol/claude-agent-acp/issues/1612))
([ddee24e](ddee24ef8f))

---
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-13 13:34:19 +00:00
Eric Holk
4f474d655b feat: report the turn's rate limit in rate-limit failure errors (#1610)
When a turn fails with `errorKind: "rate_limit"`, the error's `data` now
also carries the turn's latest `rate_limit_event` info as `rateLimit`:

```json
"data": {
  "errorKind": "rate_limit",
  "rateLimit": { "status": "rejected", "rateLimitType": "five_hour", "resetsAt": 1763002200 }
}
```

## Why

ACP clients may want to handle a rate-limit failure better than showing
its message, for example by showing when the limit resets or offering to
continue the turn then. That needs the reset time as data. Today the
adapter forwards `rate_limit_event` info only on a `usage_update`
(`_meta["_claude/rateLimit"]`). A message sent while the account is
already over its limit is refused before its first usage, so no
`usage_update` goes out and the client gets only:

```json
"error": { "code": -32603, "message": "Internal error: You've hit your 5-hour limit · resets 5:30pm (America/Los_Angeles)", "data": { "errorKind": "rate_limit" } }
```

Without this change, the only way to get the reset time is parsing that
text.

## What changed

- `turnErrorData` in `src/result-end.ts` builds the `data` of the error
that fails a turn. It adds `rateLimit` only for a `rate_limit` failure
in a turn that saw a `rate_limit_event`. Other failures keep their
current data.
- v2: `TurnReport` records the turn's latest `rate_limit_event` info and
clears it on `reset()`. `idleOutcome` passes it to `turnErrorData`.
- v1: the prompt loop tracks `turnRateLimit` alongside
`lastAssistantError`, resets it with the other per-turn scratch state,
and passes it at the three result-failure sites that used
`errorKindData(lastAssistantError)`.

## Testing

- New v1 and v2 tests: a `rate_limit_event` followed by a `rate_limit`
failure puts `rateLimit` in the error data.
- `npm run build`, `npm run check` and `npm run test:run` pass.
- Manually tested with a locally built binary: a client received
`rateLimit` in the error for a message refused at the 5-hour limit.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 13:31:11 +00:00
Ben Brandt
21f3e0430b fix: end the tool calls of a response that Claude Code took back (#1616)
## Summary

Follow-up to #1612. When a model refuses, Claude Code takes back the
response it already sent and retries on the fallback model. A tool call
of that response which has no result yet is stopped, and no result ever
comes for it. The adapter never read which messages were taken back, so
that tool call stayed open:

- **v1:** until the turn's end, where the prompt then failed with
`incomplete_tool_call`, although the retry had answered.
- **v2:** past the turn's idle, since a call that ran is left open for
its result.

Both cores now end the call as soon as it is named: `failed` on v1,
`cancelled` on v2, with a text that says why.

## What Claude Code sends

Read from the code of CLI 2.1.323. A refusal fallback cannot be produced
on demand, so none of this was observed live.

- The refused response reaches the host as usual: its stream and its
`assistant` frames, which can hold `tool_use` blocks, and the results of
tools that had finished.
- Claude Code then stops the tools that had not finished and sends no
result for them.
- It names the messages it took back, by the `uuid` their frames were
sent with, in two places: `retracted_message_uuids` of the
`model_refusal_fallback` notice, and `supersedes` of the first
`assistant` frame of the retry.

Since 2.1.323 `supersedes` also names a response whose tool call could
not be parsed. Such a response has stop reason `tool_use` and no
`tool_use` block, so it leaves no tool call open, and this change does
nothing for it.

## What changes

- `RetractedToolCalls` in `src/tool-calls/retracted.ts` (new) keeps the
tool uses of each assistant frame by `uuid`, for the last 1,000 frames,
and gives the tool calls of the messages that a frame takes back. It
reads both signals and gives each tool call once.
- `AcpToolCallRenderer.unfinished` gains a `retracted` case with its own
text: "Claude Code replaced the response that made this tool call, and
stopped the call. If it had started, some of its effects may have
happened."
- v1 closes through the existing `closeOpenToolCalls`, which now also
takes the tool call id. The change in `src/v1/acp-agent.ts` is wiring.
- v2 adds `SessionOutput.closeRetracted`, which shares one private
`close` with `closeUnfinished`.

Only a call that is still open is closed. A call that already has its
result is left alone, and a result that arrives after the close changes
nothing.

## Not in this PR

- **Superseded text stays in the transcript.** ACP v1 has no way to take
a message back. On v2 the `agent_message` upsert could clear one
(`content: []`), but that leaves an empty message whose rendering is up
to the client, and v1 and v2 would then differ. This needs a decision,
so it is left as it is.
- A tool use that streamed but whose `assistant` frame never arrived has
no `uuid` to match, so it is not closed by this.
- No scenario-harness case: its plain and Zed recordings must match the
origin/main baseline, so a deliberate difference there needs a
documented exception first.

## Testing

- `npm run build`, `npm run check`, `npm run test:run` (4,128 tests
pass)
- `src/tests/retracted-tool-calls.test.ts` for the shared class
- One `it.each` in `src/tests/v1/acp-agent.test.ts` and one in
`src/tests/v2/acp-v2.test.ts`, with one row per signal. The v1 rows fail
on main with `incomplete_tool_call`.
- The doubles follow the frames as read from the CLI's code, since the
case cannot be reproduced against the real CLI.
2026-11-13 14:27:16 +01:00
Ben Brandt
c78bcc7f41 fix: follow Claude Code's rule for managed deniedModels in the model picker (#1615)
## Summary

Follow-up to #1612. With an `availableModels` allowlist, the model
picker rebuilds its rows from the allowlist entries and drops the ones a
managed `deniedModels` list blocks. It decided that with its own rule
(`isDeniedModel`), which differed from Claude Code's in both directions.
The rule is now the SDK's `matchDeniedModels()` (alpha, 0.3.323).

This only affects a session that has both an `availableModels` allowlist
and a managed `deniedModels` list. Without an allowlist the picker shows
Claude Code's own list, which Claude Code has filtered already.

## What Claude Code really does

Measured with CLI 2.1.323: sessions started with `managedSettings: {
deniedModels }`, then `setModel(id)` for each id. No prompt was sent.
The deny check runs before the "not found" check, so a refusal that says
"is restricted by your organization's settings" means the list blocks
the id.

| Entry | Model id | `setModel` | SDK matcher | Old adapter rule |
| --- | --- | --- | --- | --- |
| `claude-opus-5-5` | `vertex_ai/claude-opus-5-5` | refused | denied |
offered |
| `claude-opus-5-5` | `anthropic/claude-opus-5.5`,
`claude-opus-5-5:free` | refused | not denied | hidden |
| `claude-opus-5-5` | `openrouter/anthropic/claude-opus-5.5` | refused |
not denied | offered |
| `claude-opus-5` | `claude-opus-5-5` | refused | denied | hidden |
| `claude-opus-4.5` | `claude-opus-4-5` | allowed | not denied | hidden
|
| `claude-opus-4.5` | `anthropic/claude-opus-4.5` | refused | not denied
| hidden |
| `claude-haiku` | `claude-haiku-5-5` | allowed | not denied | hidden |
| `my-model` | `gateway/my-model` | refused | not denied | offered |
| `my-model` | `my-model:free` | not blocked | not denied | hidden |
| `default`, `opusplan` | the same name | allowed | not denied | hidden
|

Over 169 ids in 30 sessions:

- the old rule offered 14 ids that `setModel` refuses, and hid 27 that
the list does not block;
- `matchDeniedModels` alone missed 33 refused ids. Its doc says so: it
compares an id with the entries, and the CLI also judges path tails and
a model's other names;
- the new rule agrees on all 169.

## The rule

`deniedModelIds` in `src/session-model.ts` asks `matchDeniedModels`
about three spellings of each id, so the entry rules stay the SDK's:

- the id itself;
- its last path segment, because an exact-name entry also blocks
`gateway/my-model`;
- its version spelling: everything from the first `:` dropped, and `5.5`
written `5-5`.

**Workaround to note.** The version spelling is asked behind a fixed
path, `version-of/`. The SDK ignores a path for a versioned id, and the
path keeps the spelling from equalling an exact-name entry. Without it,
the entry `my-model` would block `my-model:free`, which Claude Code
allows. This leans on how the SDK function treats paths, which nothing
documents. The tests hold recorded CLI answers and fail if the path is
removed or if only the id itself is asked.

`isDeniedModel` and its own rule are removed.
`applyAvailableModelsAllowlist` is async now, because the SDK function
is; v1 (`src/v1/acp-agent.ts`) and v2 (`src/v2/setup.ts`) each gain one
`await`. The SDK call takes under 1 ms.

## Not verified

- A custom name such as `gpt-5` answers "not found" on a first-party
account. That is read as "not denied" from the order of the checks; no
gateway was available to confirm it.
- `modelOverrides` targets were checked as plain ids given to
`setModel`.
- `matchDeniedModels().warnings` are not logged: `/status` and `claude
doctor` already list them.

## Testing

- `npm run build`, `npm run check`, `npm run test:run` (4,125 tests
pass)
- The `isDeniedModel` tests became one `it.each` in
`src/tests/model-resolution.test.ts`. Every id in it is a recorded CLI
answer.
- A second, independent run against the real CLI reproduced nine of the
recorded answers.
2026-11-13 14:26:13 +01:00
Ben Brandt
ca46b412f5 fix: log a managed policy source that did not load (#1614)
## Summary

Follow-up to #1612. Logs when a machine policy source may be there and
did not load, using `machinePolicy.didNotLoad` (alpha) that
`resolveSettings()` reports since SDK 0.3.323.

## Why

`resolveSettings()` shows a policy source that it could not read (a
macOS MDM profile, the Windows registry policy, the managed-settings
file) as absent. The adapter then applies nothing from it, and nothing
said so:

- at start-up, `applyManagedPolicyEnv` copies no env vars from it into
the process;
- in `SettingsManager`, the session's settings and model list (managed
`deniedModels`, `availableModels`, `modelOverrides`) do not reflect it.

Claude Code reads the policy itself, so its own read is not affected.
The log line only explains why the adapter's side looks as if no policy
were set.

## What changes

- `unloadedPolicySources` in `src/managed-policy.ts` names the sources
for a log line. It is undefined when every source loaded, and for an SDK
that does not report the member.
- `applyManagedPolicyEnv` writes one line to stderr and still applies
the env vars of the sources that did load.
- `SettingsManager` logs once per change, because it resolves the
settings again after every settings file edit.

Not in this PR: reading the policy again later. The env vars are applied
once at start-up, so a source that loads later is picked up for env vars
only by a restart, which the message says.

## Testing

- `npm run build`, `npm run check`, `npm run test:run` (4,126 tests
pass)
- The real SDK reports `machinePolicy: { didNotLoad: false }` on this
machine; the doubles use that shape.
2026-11-13 14:16:33 +01:00
Ben Brandt
9619805306 fix: assume a 1M context window until Claude Code reports it (#1613)
## Summary

Follow-up to #1612. The context window that `usage_update.size` reports
before Claude Code has said what the window is changes from 200K to 1M,
and the "1m" text heuristic that upgraded it is removed.

## Why

The adapter takes the window from Claude Code: a background
`getContextUsage` at session creation and at every model switch, then
each result's `modelUsage`. Until the first of those lands it reports a
placeholder, `DEFAULT_CONTEXT_WINDOW`.

Since Claude Code 2.1.323 that placeholder is the less likely value.
Asked directly (`getContextUsage`, no prompt sent):

| Model | Window |
| --- | --- |
| An id Claude Code does not know (`my-gateway-model`, `gpt-5`) |
1,000,000 |
| `opus`, `sonnet`, `haiku` | 1,000,000 |
| `claude-haiku-4-5`, `claude-sonnet-4-5` | 200,000 |
| An unknown id or `sonnet` with `CLAUDE_CODE_DISABLE_1M_CONTEXT=1` |
200,000 |

So 1M is what Claude Code itself assumes when it knows nothing about a
model, and what it reports for its current aliases.

## What changes

- `DEFAULT_CONTEXT_WINDOW` is 1,000,000.
- `inferContextWindowFromModel` is gone, with its two callers: the seed
in `immediateContextWindow` and the `message_start` upgrade in the v1
and v2 cores. It only ever turned 200K into 1M for a model whose id,
resolved id, name or description held a "1m" token. With a 1M default it
has nothing left to do, and the table shows that a model's names do not
give its window (`sonnet` is 1M, or 200K under the environment
variable).
- Nothing changes in how the real window is learned: the background
read, the per-result update and the cross-session cache are as before.

For a client: the first `usage_update` of a session or after a model
switch can carry `size: 1000000` where it carried `200000`, until the
read lands (0.2 to 0.6 s in the probe above) or, if the read fails,
until the first result. A model with a 200K window shows 1M for that
moment; before, a model with a 1M window showed 200K.

## Tests

- The session double (`mockSessionState`) starts with the default
window, as a real session does. It held 200K with
`contextWindowAuthoritative: false`, which can no longer occur.
- Tests of the heuristic are removed. Tests that tell a learned window
from the default now use values that differ from 1M.
- The scenario recordings are unchanged: their scripted Claude Code
reports a 200K window, and the read lands before the first turn.

## Testing

- `npm run build`, `npm run check`, `npm run test:run` (4,115 tests
pass; 6 tests of the removed heuristic are gone)
- Real CLI: the `getContextUsage` reads in the table above
2026-11-13 14:15:51 +01:00
Ben Brandt
ddee24ef8f fix: update to @anthropic-ai/claude-agent-sdk 0.3.323 (#1612)
## Summary

Updates `@anthropic-ai/claude-agent-sdk` from 0.3.322 to 0.3.323, which
bundles Claude Code 2.1.323. The new version needs two adapter fixes,
both included here:

- the session index reads titles by the SDK's new rule;
- the heartbeat of a long-running hook no longer lands on a tool call.

Also bumps these dev dependencies: `@anthropic-ai/sdk` 1.7.0 → 1.8.0,
`@tsconfig/node22` 22.0.6 → 22.0.7 and `prettier` 3.9.10 → 3.10.1.

`npm update` brings the in-range lockfile bumps: `rolldown` 1.2.18 →
1.2.19, `vite` 8.3.7 → 8.3.8, `hono` 4.13.17 → 4.13.18,
`@hono/node-server` 2.1.6 → 2.1.8, and patch releases of
`@babel/runtime`, `brace-expansion`, `encodeurl`,
`@jridgewell/resolve-uri` and `@rolldown/pluginutils`. `rolldown` 1.2.19
lists its wasm32 binding as an optional dependency, so the lockfile
gains `@rolldown/binding-wasm32-wasi`, `@napi-rs/wasm-runtime`,
`@emnapi/core` and `@emnapi/runtime` (2.0.0-beta.2),
`@emnapi/wasi-threads`, `@tybys/wasm-util` and `tslib`. All are dev-only
and optional.

TypeScript stays at 6.0.3: 7.0.2 is out, but `@typescript-eslint/parser`
8.72.2 still requires `typescript <6.1.0`.

## Session titles

`getSessionInfo` and `listSessions` of SDK 0.3.323 read titles
differently, and neither changelog says so. The session index mirrors
the SDK's rule (`sdkTitles` in `src/session-index/transcript-scan.ts`),
so it has to follow.

- Before: the SDK took the last `"customTitle":"…"` text of the tail or
the head, wherever it stood.
- Now: it parses each line that names the field, from the last one back,
and takes the top-level string of the record. For `customTitle` and
`aiTitle` the record must be of type `custom-title` or `ai-title`.
`lastPrompt` and `summary` may be on a record of any type.

What changes for a client:

- A `customTitle` or `lastPrompt` inside a message is no title. A tool
result that shows a transcript no longer renames the session.
- A `custom-title` record that the start of the 64 KB tail window cuts
is not read. The old rule still found its title text.
- A `custom-title` record appended to a line whose write was cut off is
read.

Checked against the real SDK: a differential run of `sdkTitles` against
`getSessionInfo` over 1,800 generated transcripts gave no mismatch. The
old rule mismatched on 371 of 600. The run is not part of this PR; the
tests in `src/tests/v1/session-index.test.ts` compare each case above
with `getSessionInfo`.

## Hook heartbeats

Since Claude Code 2.1.323, a hook that runs long sends a `tool_progress`
beat every 30 seconds. Recorded from the real CLI:

```json
{
  "type": "tool_progress",
  "tool_use_id": "hook-PreToolUse-toolu_01X…-heartbeat-0",
  "tool_name": "PreToolUse",
  "parent_tool_use_id": "toolu_01X…",
  "elapsed_time_seconds": 30,
  "heartbeat": true
}
```

For a PreToolUse or PostToolUse hook, `parent_tool_use_id` is the tool
call. For a UserPromptSubmit or Stop hook it is an id that names no tool
call. The PostToolUse beats come before the tool result.

Both cores send a beat under an unknown id to the call that
`parent_tool_use_id` names. So without a change, a slow PreToolUse or
PostToolUse hook:

- marked the tool call `in_progress` before it ran, on v1 and v2;
- restarted its elapsed time with the hook's (30 → 1 → 2 → 3 → 30 in the
scenario);
- named the call `PreToolUse` in `_meta.claudeCode.toolName` for a plain
or Zed client.

The adapter now drops these beats. This matches how it already ignores
`hook_started`, `hook_progress` and `hook_response`: hooks have no ACP
surface. The rule that picks the tool call of a beat was written twice,
in `src/v1/acp-agent.ts` and `src/v2/stream.ts`. It is now one function,
`progressToolCallId` in `src/tool-calls/renderer.ts`, which holds the
new rule too.

The `tool-progress` scenario carries a PreToolUse and a PostToolUse
beat. Its recordings for the plain, Zed, AIR and v2 clients are
unchanged, and all four fail without the fix.

## Changelog review

Read the SDK changelog for 0.3.323 and the Claude Code changelog for
2.1.323. No other entry needs a code change:

- **Types.** Doc text and new optional members only, plus one new
`SDKStartupFailureReason` (`managed_settings_not_delivered`).
`startupFailureError` treats a reason it does not list as an internal
error with the CLI's message.
- **Context window of an unknown model id**, 200K → 1M. The adapter
reads the window from Claude Code (`getContextUsage`) at session
creation and at every model switch, so it reports the new value. Its own
200K is only the placeholder until that read lands.
- **`canUseTool` suggestions** no longer hold a Bash allow rule for a
write to `.git` or `.claude`. The adapter then offers no "always allow"
option.
- **`init.plugin_errors`** also lists load errors of plugins that
managed settings enable. The adapter only logs them.
- **`forkSession` with `upToMessageId`** and **`getSessionMessages`**
compaction fixes. Picked up as they are: the adapter has no workaround
for the old behavior.
- **A subagent's WebSearch result can carry `tool_use_result`.** The
code reads only fields that this result does not have. One comment in
`src/async-tasks.ts` said that no subagent result carries one; it now
says Bash results.
- **A permission check that fails after a PreToolUse hook allowed the
call** goes to `canUseTool`, which the adapter always provides.

## Not in this PR

Left for follow-ups, because each is its own change:

- The 200K placeholder for the context window. 1M is now the more likely
value, and with it the "1m" text heuristic has nothing left to do.
- `matchDeniedModels()` (alpha). The adapter's own `isDeniedModel`
disagrees with it on 23 of 875 model and entry pairs.
- `supersedes` on the retry of a response whose tool call could not be
parsed. The adapter does not act on `supersedes`.
- `machinePolicy.didNotLoad` (alpha) of `resolveSettings()`.

## Testing

- `npm run build`, `npm run check`, `npm run test:run` (4,121 tests
pass)
- Real CLI: one short session with slow hooks and
`CLAUDE_CODE_HOOK_HEARTBEAT_INTERVAL_MS=1000`, to record the heartbeat
frames and their order
- Real CLI: `getContextUsage` for unknown and known model ids, without a
prompt
2026-11-13 13:56:39 +01:00
Ben Brandt
4804917a6f feat: report unknown subagent state at a conversation reset and reference children from tool calls (#1608)
Brings the RFD form of subagent reporting in line with the ACP [Subagent
Sessions RFD](https://agentclientprotocol.com/rfds/subagents), which is
moving to Preview in agentclientprotocol/agent-client-protocol#2800. The
related RFD clarifications merged in
agentclientprotocol/agent-client-protocol#2801. AIR output is unchanged,
and no scenario snapshots changed.

## Changes

- **`unknown` at a conversation reset.** A child that is still working
at `conversation_reset` now reports `unknown` and `capabilities: null`,
not `idle`. The adapter forgets its children at reset, so it can no
longer observe or stop them. v1 reports it on the parent's
`subagent_update`. v2 also sends `state_update` `unknown` on the child.
- Stream end, stream error, `queryEnded`/`retire`, and `worker_restart`
keep `idle`, because the work is known to have stopped.
- In v2, the forgotten child's unfinished tool calls keep their last
status. They are no longer forced to `failed`.
- **`session` tool call content.** Added live and in replay, for the v1
RFD form and v2, only when the child is already announced:
  - `TaskStop` whose `task_id` is a child's agent id.
  - `SendMessage` that stays visible and targets a child's agent id.
- v1 only: an Agent/Task launch that a permission request keeps visible.
- The reference is repeated on every update that carries content,
because `content` replaces the whole array.
- **Docs.** The README's subagent section matches the code: SendMessage
is hidden and reported as linked messages, `unknown` at reset, `session`
content, and no `session_message_chunk`. Three stale comments are fixed.

## Workarounds and limits

- **Post-response ordering still uses `setTimeout(0)`.** SDK 2.6.0 gives
`onRequest` handlers no responder or after-response hook. The comments
in `src/v2/agent.ts` and `src/v1/acp-agent.ts` now state what the
ordering relies on: the response is queued in the microtask after the
handler returns, and `Connection.sendMessage` is first-in, first-out.
The SDK does not promise this.
- **The `unknown` change assumes Claude Code keeps subagents running
across `/clear`.** That comes from the existing v2 comment and test
double. It was not checked against the real CLI.
- No reference is added when a tool names a child by agent name, or for
v2 launch calls that a permission request shows.
- A child that is idle at reset keeps its `cancel` capability. Unchanged
here.
- A stream error with a client-supplied `spawnClaudeCodeProcess` still
reports `idle`, although closing may not stop a remote CLI.

## Testing

- `npm run build`: clean.
- `npm run check`: clean.
- `npm run test:run`: 73 files passed, 5 skipped. 4117 tests passed, 33
skipped.
- Integration tests against the real CLI were not run.
2026-11-12 15:48:47 +00:00
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
Ben Brandt
6bc2e058cb feat: serve session/status on v1 and v2 (#1584)
Implements the unstable [Session status
RFD](https://agentclientprotocol.com/rfds/session-status) on both
agents, and advertises `status: {}` in the v1 `sessionCapabilities` and
the v2 `capabilities.session`.

## What it answers

| Session | v1 | v2 |
| --- | --- | --- |
| Held by this connection | `open` | `open` with `state` |
| Not held, non-empty transcript stored | `saved` | `saved` |
| Not held, no transcript | `-32002` with `data.sessionId` | same |
| Subagent session | `-32002` | `-32602`, as resume, close, fork and
delete of one |

- The request starts no query, sends no update, and waits for no resume,
restart or close.
- On v2, `state` is the foreground state as a `state_update` snapshot
holds it: with the session's `background` list and without a stop
reason. `V2Background.snapshot` builds it without marking the list as
reported, so a status request does not change which later
`state_update`s go out.
- `saved` or unknown is decided in the new shared
`src/session-status.ts`: a non-empty `<sessionId>.jsonl` in any project
directory. The request names no `cwd`, so a resume with another `cwd`
can still fail with `-32002`. A directory that cannot be read for
another reason than being absent fails the request and does not answer
`-32002`.

## Behaviour to review

- **A v1 session whose query ended reads `open`.** The connection still
holds it, and a `session/load` with the same params returns it as it is,
so `saved` would point the client at a resume that revives nothing. Its
prompts fail with the "session ended" error. v2 drops such a session and
answers from its transcript.
- **A session that was never prompted is unknown once it is closed.**
Claude Code writes the transcript at the first prompt, and a resume of
it fails with `-32002` too.
- **A subagent session does not advertise `status`,** so v2 refuses the
request like its other requests, and v1 does not know the id.
- **`open` is per connection.** Each connection has its own agent and
its own Claude Code processes.

## Not done

- The lookup does one `readdir` and one `stat` per project directory for
each probe of a session that is not held. Sharing the v1 session index's
cached scan is a follow-up.
- Not run against the real CLI (`npm run test:integration`).

## Other changes

- `isSessionId` and `projectsRoot` moved from
`src/v1/session-index/project-dirs.ts` to `src/paths.ts`, so the shared
module can use them.
- The 40 v2 and 2 AIR scenario golden files gain the capability in
`initialize`, and the origin/main comparison allows it, next to
`systemPrompt` and `auth.status`.
- The README lists `session/status` in the v2 surface.
2026-11-10 18:11:39 +00:00
132 changed files with 5946 additions and 1508 deletions

View File

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

View File

@@ -1,5 +1,57 @@
# Changelog
## [0.107.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.106.0...v0.107.0) (2026-11-13)
### Features
* report the turn's rate limit in rate-limit failure errors ([#1610](https://github.com/agentclientprotocol/claude-agent-acp/issues/1610)) ([4f474d6](https://github.com/agentclientprotocol/claude-agent-acp/commit/4f474d655b88a075ec7d0124d45cafa0f5565af7))
* report unknown subagent state at a conversation reset and reference children from tool calls ([#1608](https://github.com/agentclientprotocol/claude-agent-acp/issues/1608)) ([4804917](https://github.com/agentclientprotocol/claude-agent-acp/commit/4804917a6fa7050490d1d62c6689e78c231c752a))
* say the cause of an api retry in the AIR warning details ([#1594](https://github.com/agentclientprotocol/claude-agent-acp/issues/1594)) ([4f690e1](https://github.com/agentclientprotocol/claude-agent-acp/commit/4f690e129c01f22e2c2a87e0e0a3c5b503241f15))
### Bug Fixes
* assume a 1M context window until Claude Code reports it ([#1613](https://github.com/agentclientprotocol/claude-agent-acp/issues/1613)) ([9619805](https://github.com/agentclientprotocol/claude-agent-acp/commit/96198053067cf4d7b7440c52036174ad4f54183e))
* end the tool calls of a response that Claude Code took back ([#1616](https://github.com/agentclientprotocol/claude-agent-acp/issues/1616)) ([21f3e04](https://github.com/agentclientprotocol/claude-agent-acp/commit/21f3e0430b693e6b04750ab3018e1eaf726af173))
* explain why Claude Code turns Fast mode off ([#1602](https://github.com/agentclientprotocol/claude-agent-acp/issues/1602)) ([c331e17](https://github.com/agentclientprotocol/claude-agent-acp/commit/c331e178a5df47448be1a3ed2f71270c3aad2433))
* follow Claude Code's rule for managed deniedModels in the model picker ([#1615](https://github.com/agentclientprotocol/claude-agent-acp/issues/1615)) ([c78bcc7](https://github.com/agentclientprotocol/claude-agent-acp/commit/c78bcc7f417fc8d3da66f51021b43f4528826ec0))
* log a managed policy source that did not load ([#1614](https://github.com/agentclientprotocol/claude-agent-acp/issues/1614)) ([ca46b41](https://github.com/agentclientprotocol/claude-agent-acp/commit/ca46b412f51cdb7b5a303c751f5d48c216700887))
* report a mid-turn API error notice where the response broke off ([#1599](https://github.com/agentclientprotocol/claude-agent-acp/issues/1599)) ([4df6999](https://github.com/agentclientprotocol/claude-agent-acp/commit/4df69996ca74adff6c42fc60e4b9b742f2d3fdf5))
* say why the login failed in the v2 authRequired error ([#1598](https://github.com/agentclientprotocol/claude-agent-acp/issues/1598)) ([ecd066c](https://github.com/agentclientprotocol/claude-agent-acp/commit/ecd066c8700955e3456dd9387e6b8317f024e26c))
* update to @anthropic-ai/claude-agent-sdk 0.3.323 ([#1612](https://github.com/agentclientprotocol/claude-agent-acp/issues/1612)) ([ddee24e](https://github.com/agentclientprotocol/claude-agent-acp/commit/ddee24ef8fd0abc0401fb32d567914030fbf6944))
## [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

@@ -53,81 +53,6 @@ npm run build:binary
npm run smoke:binary # add `-- --full` to drive a real session; needs a Claude login
```
### Subagent sessions
A client that declares `clientCapabilities.subagents`, other than JetBrains AIR, gets each Agent or
Task subagent as its own child session, following the [subagents RFD](https://github.com/agentclientprotocol/agent-client-protocol/blob/main/docs/rfds/subagents.mdx):
- `subagent_update` on the parent announces the child before any of its traffic, with the
parent's `title` and `description` for it, and reports its work state: `running`,
`requires_action` while one of its requests is open (and `running` again when the last one ends,
unless a cancel withdrew it: the child is stopping then), and `idle` when its work ends, with
`end_turn` or `cancelled` when the SDK gives the reason. A failure is `idle` with the `error`
stop reason and the SDK's error text as its `error`. The child keeps its session for every
delegation to it.
- A child that runs outside its parent's working directory, such as an Agent call with
`isolation: "worktree"`, gets its `cwd` on `subagent_update` before its first tool call. The
adapter takes it from the `cwd` of the child's own hooks: its SubagentStart hook runs before its
first model request. It sends `cwd: null` when the child is back in its parent's directory. A
child in its parent's directory gets no `cwd`, and no child gets `additionalDirectories`: Claude
Code gives a subagent the session's additional directories, so it inherits them. A replay
announces a child with the `cwd` that its transcript recorded, and clears it after the response
when that directory no longer exists.
- Session messages replace the Agent or Task call that launches the child, which its parent is
not shown:
- The launch's prompt is a `session_message` in both transcripts: the parent's outgoing view and
the child's incoming one. Both use the launch's tool use id, and each names the other with the
draft RFD's `recipientMessageId` and `senderMessageId`.
- A foreground child's answer, from the launch's result, is the child's `session_message` to
the parent. In Claude Code's hand-back mode, the child's `SubagentHandback` call is its
message to the parent instead, in both transcripts and linked the same way.
- A launch whose child is never exposed is shown as a tool call after all, with its final
status.
- The message of a SendMessage call that resumes the child is a `session_message` in the child's
transcript. The SendMessage call stays a tool call of the session that made it.
- `session/cancel` on the parent interrupts Claude Code, which stops the foreground children that
the turn waits on. Background children keep running, as the RFD prefers; a client stops one with
its own `cancel`. Each stopped child reports `idle` with `cancelled` from Claude Code's own
events, and its open requests are withdrawn. The adapter ends no child at a cancel itself, so a
child that keeps running keeps its open requests and its open tool calls. To spare background
children, the adapter declares the SDK option `perTaskStopAffordance`, which a client can turn
off in `_meta.claudeCode.options`. That option spares background workflows too, which a client
cannot stop, so the adapter stops them after the interrupt, as Claude Code did without it.
Background commands keep running after a cancel either way. A tool call of a stopped child that Claude Code reports no
result for, such as the command it was running, ends in the child's session before the cancelled
prompt resolves. When Claude Code stops a background child after that, or while no prompt runs,
the tool call ends right after the child reports `idle`. A child that still runs when Claude Code
exits is `idle` with no stop reason.
- A child whose parent is not yet known when it ends is not exposed, since the RFD forbids
guessing its parent.
- A replay restores each child under its live session id, from its own transcript, with the same
session messages as live, and its end is an `idle` before the response.
A launch without Claude Code's metadata file for the child stays an ordinary tool call of the
parent, and a notice, for a client that takes notices, says that subagent history is missing.
- After a load or resume, with or without replay, a SendMessage that resumes an agent from before
it runs that agent in its session from before. Before the agent's traffic, each of its ancestors
that the client was not told about in this connection is announced, parent before child and
with no work state, as the RFD requires. An agent whose launch the session's history does not
record stays unexposed.
- Each running child is announced with the `cancel` capability. `session/cancel` with the child's
session id stops that child's current work, through the SDK's `Query.stopTask`, and not its
parent's: Claude Code also stops the foreground children it waits on, and the parent's turn goes
on. Each stopped child reports `idle` with `cancelled` from Claude Code's own events, and its
requests that the stop withdraws report no `running` first. A replayed child, which no longer
runs, gets no capabilities. A client cannot prompt or otherwise change a child.
An [ACP v2](#experimental-acp-v2) client gets the same without declaring anything, since subagents
are part of v2's baseline. Each child also reports its work state as its own `state_update`, right
after the parent's copy.
JetBrains AIR keeps the earlier draft of the RFD (`subagent_spawned`, `subagent_state_update`),
which it enables with `nativeSubagentSessions` in `_meta.jetbrains.air.capabilities`. See
[AIR extensions](docs/air-extensions.md#native-subagent-sessions).
Without either signal, Agent and Task stay ordinary tool calls and child interactions stay on the
root session. Clients that use the historical `_meta["subagent-transcript"]` capability or the
`forwardSubagentText` session option retain the flattened child transcript.
### Experimental ACP v2
The adapter also serves the [draft ACP v2](https://agentclientprotocol.com/protocol/v2/overview):
@@ -140,9 +65,10 @@ 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`, 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.
[subagents](docs/subagent-sessions.md). Steering is v1 only for now.
v2 is served by the v2 agent in `src/v2/`, which runs straight on Claude
Code instead of translating to v1. A v2 client that declares JetBrains AIR

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.

93
docs/subagent-sessions.md Normal file
View File

@@ -0,0 +1,93 @@
# Subagent sessions
A client that declares `clientCapabilities.subagents`, other than JetBrains AIR, gets each Agent or
Task subagent as its own child session, following the [subagents RFD](https://github.com/agentclientprotocol/agent-client-protocol/blob/main/docs/rfds/subagents.mdx):
- `subagent_update` on the parent announces the child before any of its traffic, with the
parent's `title` and `description` for it, and reports its work state: `running`,
`requires_action` while one of its requests is open (and `running` again when the last one ends,
unless a cancel withdrew it: the child is stopping then), and `idle` when its work ends, with
`end_turn` or `cancelled` when the SDK gives the reason. A failure is `idle` with the `error`
stop reason and the SDK's error text as its `error`. The child keeps its session for every
delegation to it.
- A child that still works when Claude Code starts a fresh conversation, such as for `/clear`, is
`unknown`, and loses its `cancel` capability. Claude Code can keep such a child running, but the
adapter forgets the children of the old conversation, so it reports nothing more for it and
cannot stop it. Its unfinished tool calls keep their status. An idle child keeps its state.
- A child that runs outside its parent's working directory, such as an Agent call with
`isolation: "worktree"`, gets its `cwd` on `subagent_update` before its first tool call. The
adapter takes it from the `cwd` of the child's own hooks: its SubagentStart hook runs before its
first model request. It sends `cwd: null` when the child is back in its parent's directory. A
child in its parent's directory gets no `cwd`, and no child gets `additionalDirectories`: Claude
Code gives a subagent the session's additional directories, so it inherits them. A replay
announces a child with the `cwd` that its transcript recorded, and clears it after the response
when that directory no longer exists.
- Session messages replace the Agent or Task call that launches the child, which its parent is
not shown:
- The launch's prompt is a `session_message` in both transcripts: the parent's outgoing view and
the child's incoming one. Both use the launch's tool use id, and each names the other with the
RFD's `recipientMessageId` and `senderMessageId`.
- A foreground child's answer, from the launch's result, is the child's `session_message` to
the parent. In Claude Code's hand-back mode, the child's `SubagentHandback` call is its
message to the parent instead, in both transcripts and linked the same way.
- A launch whose child is never exposed is shown as a tool call after all, with its final
status.
- A SendMessage call that resumes the child is not shown either. Its message is a
`session_message` in both transcripts, the sender's outgoing view and the child's incoming one,
linked like a launch's prompt. A SendMessage call whose message resumes no announced child,
such as one to the parent, is shown as a tool call, with its final status.
- Every message is one whole `session_message`. The adapter sends no `session_message_chunk`: it
has each message's full text before it reports it, from the input or the result of the tool call
that sent it.
- A tool call that the client is shown and that operates on an announced child of another session
carries the RFD's `session` content, `{ "type": "session", "sessionId": "<child>" }`, after its
other content: a TaskStop call whose `task_id`, or a SendMessage call whose `to`, is the child's
agent id; a SendMessage call that resumed the child and stays shown because a permission request
showed it first; and, on v1, an Agent or Task call that stays shown for the same reason. A
`tool_call_update` without content carries none, since it would replace the content the client
holds. A call that names the child by its name gets no reference: Claude Code resolves the
name, and the adapter cannot tell which agent it picked.
- `session/cancel` on the parent interrupts Claude Code, which stops the foreground children that
the turn waits on. Background children keep running, as the RFD prefers; a client stops one with
its own `cancel`. Each stopped child reports `idle` with `cancelled` from Claude Code's own
events, and its open requests are withdrawn. The adapter ends no child at a cancel itself, so a
child that keeps running keeps its open requests and its open tool calls. To spare background
children, the adapter declares the SDK option `perTaskStopAffordance`, which a client can turn
off in `_meta.claudeCode.options`. That option spares background workflows too, which a client
cannot stop, so the adapter stops them after the interrupt, as Claude Code did without it.
Background commands keep running after a cancel either way. A tool call of a stopped child that Claude Code reports no
result for, such as the command it was running, ends in the child's session before the cancelled
prompt resolves. When Claude Code stops a background child after that, or while no prompt runs,
the tool call ends right after the child reports `idle`. A child that still runs when Claude Code
exits is `idle`, since its work ended with the process: with no stop reason on v1, and on v2
with the `error` stop reason and the error that ends the root's work.
- A child whose parent is not yet known when it ends is not exposed, since the RFD forbids
guessing its parent.
- A replay restores each child under its live session id, from its own transcript, with the same
session messages and `session` references as live, and its end is an `idle` before the response.
Each replayed child's current state follows the response.
A launch without Claude Code's metadata file for the child stays an ordinary tool call of the
parent, and a notice, for a client that takes notices, says that subagent history is missing.
- After a load or resume, with or without replay, a SendMessage that resumes an agent from before
it runs that agent in its session from before. Before the agent's traffic, each of its ancestors
that the client was not told about in this connection is announced, parent before child and
with no work state, as the RFD requires. An agent whose launch the session's history does not
record stays unexposed.
- Each running child is announced with the `cancel` capability. `session/cancel` with the child's
session id stops that child's current work, through the SDK's `Query.stopTask`, and not its
parent's: Claude Code also stops the foreground children it waits on, and the parent's turn goes
on. Each stopped child reports `idle` with `cancelled` from Claude Code's own events, and its
requests that the stop withdraws report no `running` first. A replayed child, which no longer
runs, gets no capabilities. A client cannot prompt or otherwise change a child.
An [ACP v2](../README.md#experimental-acp-v2) client gets the same without declaring anything, since subagents
are part of v2's baseline. Each child also reports its work state as its own `state_update`, right
after the parent's copy.
JetBrains AIR keeps the earlier draft of the RFD (`subagent_spawned`, `subagent_state_update`),
which it enables with `nativeSubagentSessions` in `_meta.jetbrains.air.capabilities`. See
[AIR extensions](air-extensions.md#native-subagent-sessions).
Without either signal, Agent and Task stay ordinary tool calls and child interactions stay on the
root session. Clients that use the historical `_meta["subagent-transcript"]` capability or the
`forwardSubagentText` session option retain the flattened child transcript.

384
package-lock.json generated
View File

@@ -1,16 +1,16 @@
{
"name": "@agentclientprotocol/claude-agent-acp",
"version": "0.104.1",
"version": "0.107.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@agentclientprotocol/claude-agent-acp",
"version": "0.104.1",
"version": "0.107.0",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "2.5.0",
"@anthropic-ai/claude-agent-sdk": "0.3.322",
"@agentclientprotocol/sdk": "2.6.0",
"@anthropic-ai/claude-agent-sdk": "0.3.323",
"diff": "9.0.0",
"zod": "4.6.7"
},
@@ -18,9 +18,9 @@
"claude-agent-acp": "dist/index.js"
},
"devDependencies": {
"@anthropic-ai/sdk": "1.7.0",
"@anthropic-ai/sdk": "1.8.0",
"@eslint/js": "10.0.1",
"@tsconfig/node22": "22.0.6",
"@tsconfig/node22": "22.0.7",
"@types/node": "26.6.6",
"@typescript-eslint/eslint-plugin": "8.72.2",
"@typescript-eslint/parser": "8.72.2",
@@ -29,7 +29,7 @@
"eslint": "10.14.0",
"eslint-config-prettier": "10.1.8",
"globals": "17.14.0",
"prettier": "3.9.10",
"prettier": "3.10.1",
"typescript": "6.0.3",
"vitest": "5.1.0"
},
@@ -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",
@@ -53,22 +53,22 @@
}
},
"node_modules/@anthropic-ai/claude-agent-sdk": {
"version": "0.3.322",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.322.tgz",
"integrity": "sha512-r88ij7R3SPV2j3BtFuavgrbWZNE8SVVS42AOQ1KxlS+QnSwNNnUhmVXV3Es2dniZySBYX8ENebL43ffhiB0azw==",
"version": "0.3.323",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.323.tgz",
"integrity": "sha512-GXIczMzzceNWB+bIImK59hWOQoBWZtSw+AESPc/qbAWmmHM8Wrqb5KvTwWZ07CmjyC/KcPC47nOLxxyHlWqKPA==",
"license": "SEE LICENSE IN README.md",
"engines": {
"node": ">=18.14.0"
},
"optionalDependencies": {
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.322",
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.322",
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.322",
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.322",
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.322",
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.322",
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.322",
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.322"
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.323",
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.323",
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.323",
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.323",
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.323",
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.323",
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.323",
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.323"
},
"peerDependencies": {
"@anthropic-ai/sdk": ">=0.93.0",
@@ -77,9 +77,9 @@
}
},
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": {
"version": "0.3.322",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.322.tgz",
"integrity": "sha512-LoKkldW2zkLfaOLQD44mL1Ot4Aup8dBIo356LEeO3v2vF7JVNhMllQIXhhGpbYRz57QaYsGvOe0Kb4SbssHwcg==",
"version": "0.3.323",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.323.tgz",
"integrity": "sha512-hWkXHsSMIv+78duw3mDi0bndCpWquGuMrCX6C7G9LzEtpcd+ZvzI5ZE4E6SwqX1bSy8rxhEg+hVJSTk4ABoxpA==",
"cpu": [
"arm64"
],
@@ -90,9 +90,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": {
"version": "0.3.322",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.322.tgz",
"integrity": "sha512-rTvLS9G1OzjMIaSXrJglQtmRtVcaRmMbJQC+ND02+atCA463ac7Ty/Wc6tyzBGk8zFFGKWnfAhKtLZTNg91eMg==",
"version": "0.3.323",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.323.tgz",
"integrity": "sha512-JHtMk+FBRQPvH046dUIda6s6bza8CRQkaAdy96JwV1zqgi3mtlAg5yXFxPA/QNoiHz5Ul225wCZr9lR7gbwkMg==",
"cpu": [
"x64"
],
@@ -103,9 +103,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": {
"version": "0.3.322",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.322.tgz",
"integrity": "sha512-H1v33+MxB5JMPlnJLZffiOQ+6muxyA7tgHPyYQ+So4TH7uRhaPcjv2X03CUo5U9gXleO6u+X8ngNNDRFtfUaSg==",
"version": "0.3.323",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.323.tgz",
"integrity": "sha512-ONhCJapk9UxpaZ3HY1GTZezMoiQ6Ab7fIubS+dJeXIyj8aQjGQALHQoEl9D1Hrr+q5ppZPOAt0hg69x8aGbTBA==",
"cpu": [
"arm64"
],
@@ -119,9 +119,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": {
"version": "0.3.322",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.322.tgz",
"integrity": "sha512-zvgvVA/Mxpu1QttICZ1DGDlmgGo62HdVgKg6WbDGDQ3dgxtRC/kpCIepjtGEL8O1iO5NRxQ4mv8okLwCFcyrLQ==",
"version": "0.3.323",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.323.tgz",
"integrity": "sha512-N+ZTBRQj8Ew3/eLCZE7JADceyN6Yi1CncJAdzknF5RL9dckIujv4B7C9neBNOXdo+bGFEysjPcUbVsaRkPguKQ==",
"cpu": [
"arm64"
],
@@ -135,9 +135,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": {
"version": "0.3.322",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.322.tgz",
"integrity": "sha512-M/i7uV5wQf1kvjiPJr2KF5PeA9BpftUpMTtwE+DpQ0Tj+scthQXSMsEhDvau/Rh/MfFOc+UHED4tltignmA5+Q==",
"version": "0.3.323",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.323.tgz",
"integrity": "sha512-9M0rnLcCa9sJdZoRCPbjyMOv3raoJm39u348t2ozCgE35Y9e/jGgxy+rG2jfRTaCBILDDrR57ClRQVRlpNeLpQ==",
"cpu": [
"x64"
],
@@ -151,9 +151,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": {
"version": "0.3.322",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.322.tgz",
"integrity": "sha512-jprUZMcQ3XcpoxwKRQ0fi3qAM1zYcytmNGhlD0gWDknbbSu8di5jLU7231ihJrCr4gbbtDu3FiFUd6w9EBEjfw==",
"version": "0.3.323",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.323.tgz",
"integrity": "sha512-QMaFszyJraPnZyJTy6K9vr1wVA/4MIjWSFcaByGv/x3Hk/F7smsMnqpuATO6M6bRsev+IVPj0p6rsdpYAdt1yA==",
"cpu": [
"x64"
],
@@ -167,9 +167,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": {
"version": "0.3.322",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.322.tgz",
"integrity": "sha512-o5sPmBKT4NOglODC45wSTFmjD1AQIboC9NdGFQFOTa8kMF3bok+YqpB0DF81RmMDf3iR5tieonZvOXdYm1TDjQ==",
"version": "0.3.323",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.323.tgz",
"integrity": "sha512-FS+eYSMU/x8Hmy98zypoOw9/+jMgtGmBEyYmE+jJUCdb3a4k1pwcx08XGici/hnBR3xiKK65ifxwVg2oNyZocQ==",
"cpu": [
"arm64"
],
@@ -180,9 +180,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": {
"version": "0.3.322",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.322.tgz",
"integrity": "sha512-gONOkYx76of3p73opgWAT9gqpV7PRL8+az3vKoWXLc4xaDW6vhZF7fixpCnLDeUmPWxjXhnQxf30WGpKneTzNw==",
"version": "0.3.323",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.323.tgz",
"integrity": "sha512-mW2h1o0Kt92AKZBr05AwE3izJTAYpWNUurOppPdzH/hditbT1zwc0tAJ3p+G+XMkqUWenJNbpunVG0utfQ2hmA==",
"cpu": [
"x64"
],
@@ -193,9 +193,9 @@
]
},
"node_modules/@anthropic-ai/sdk": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-1.7.0.tgz",
"integrity": "sha512-Z+0c1iHQozlt4ymMHapPXEXfFnSbPEWGnSKbkavt+qeyaMQ3Sd6QNxfQj7vyhHYqiqYadC9shrXub8ISKf7xjQ==",
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-1.8.0.tgz",
"integrity": "sha512-yDOArk2+Qpd/djEKmZjrv7JZ+pn5iBHARJ9PAdvfhGf4H5wNqTgY/2K0/yDKdQdm8G8hrNQchMlBAHpPK7yBfg==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api": "^1.4.1",
@@ -215,9 +215,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.29.13",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.13.tgz",
"integrity": "sha512-v+oN26xP4YIlQsbQ/CbTH1WG5m/l/DjKKZNy/Kp3f/AFvbgt7Em/D0FeMM3O2hX0YzeN2M16W8lOpyhIkfCBcQ==",
"version": "7.29.14",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.14.tgz",
"integrity": "sha512-Y5tqevQaUAZhhj82BpYMNC1ssvLQTVAfwKdm8iy8qwmhvC2/CpcAu8OC+QEtjM7pMk7knx83u1Qj8gzkicyelw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -247,6 +247,43 @@
"keyv": "^5.6.0"
}
},
"node_modules/@emnapi/core": {
"version": "2.0.0-beta.2",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-beta.2.tgz",
"integrity": "sha512-MzCVoWtcg4Pm2B3je5mMvYMvj3MmBKw+fU7vbsJc9/iT8ip6/eYIHo/Dsm/mr+wsHB6SEyJDuxB09Yy6r/8+4A==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@emnapi/wasi-threads": "2.2.0",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "2.0.0-beta.2",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-beta.2.tgz",
"integrity": "sha512-JcrSPzPvZuNZvJqPTF9Fw25LbOJ8avzFVlaf2grmH4Cku0oGP47tRgqPZadcfj3YoZMHkFVnCYFbQM8I+i5QEw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.2.0.tgz",
"integrity": "sha512-JP7OYH38Q7+pyTAe14raZHLQIK7BDKbdwji+Xi3pcd/6QkJYQH3SX4wu6EplGrGJWRepoNzedfm0G3B9Z4e9Rw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
@@ -363,9 +400,9 @@
}
},
"node_modules/@hono/node-server": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.6.tgz",
"integrity": "sha512-p/KH9MnfNcsm3sR6x4lNgm5j+QN4Et8srKredl9DZfXiyZ1UQ3aQ2YVpFLbqktYN5JI+DB+VZFKM2PqE5hLKjQ==",
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.8.tgz",
"integrity": "sha512-jdf7FHh6A1lzjpHHQUgxsg2GeER2vMetnAOfeActzcoli6jamlK+NAwL6Dsr5Fl3BS3tYUQzCdtZjU8Qxv7bEA==",
"license": "MIT",
"peer": true,
"engines": {
@@ -442,9 +479,9 @@
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.3.tgz",
"integrity": "sha512-lhTtjtoEArWBjismtfS761S9Hlyn+nIZqOZVYoZ1Xn82PMNMVnk+jj/NhC4fAXGTs7ydONcJiR5vyQBTMDu27g==",
"dev": true,
"license": "MIT",
"engines": {
@@ -534,6 +571,29 @@
}
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.6.tgz",
"integrity": "sha512-mRyDmEw9/fCDXCGfL1sbgIw/GUK0PVxRW4YwgPN61F3LQ+fSyoBFarJVzgGyhF6kRzEQZ0FejDqm4WLXSeqQmw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@tybys/wasm-util": "^0.10.3"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=23.5.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
},
"peerDependencies": {
"@emnapi/core": "^1.7.1 || 2.0.0-beta.2",
"@emnapi/runtime": "^1.7.1 || 2.0.0-beta.2"
}
},
"node_modules/@opentelemetry/api": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.10.0.tgz",
@@ -555,9 +615,9 @@
}
},
"node_modules/@rolldown/binding-android-arm-eabi": {
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.18.tgz",
"integrity": "sha512-ubTG+4KlidvioxjOFdsdEa2F6JVGZb8m2ByJUqQ7fYzUeT1cEn194POcsQ10PKH4p//q0YQn4SB3YpSuzcA1Yg==",
"version": "1.2.19",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.19.tgz",
"integrity": "sha512-e+Ebe0WT+9tbXmNKPIiC8YrWAHYwug3m6i4p3rZGGulfZri1jtI4HslT+OZnFf/OIHowWF71DTlHwXBP2SiA7A==",
"cpu": [
"arm"
],
@@ -573,9 +633,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.18.tgz",
"integrity": "sha512-2Xa1t/JeFGTZn5TbzgPppcCftF8sCVnTNvrIvoSdcZhoFzAuVy1L3xxQUQ4sfWDtwVzg1u6Yi1R3SlJugRNV2Q==",
"version": "1.2.19",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.19.tgz",
"integrity": "sha512-hOombrTJ+szt2ciEzS7oCA78lMPBgF5UMQWCJIyNfnrI7GLl8HYoFqCnpL2gcPOaXMqryGCU5ExdQKyDDcC+yA==",
"cpu": [
"arm64"
],
@@ -591,9 +651,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.18.tgz",
"integrity": "sha512-iUzw9HshVvrWci9gVXjxWS7/M/IcLvNo67vu2c/w9zKBz4hIvPRixdUpIW/iCfhldLaq9ER2hecsI9UY5z0/yA==",
"version": "1.2.19",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.19.tgz",
"integrity": "sha512-XoMA2pwCKGTQzUjPgtRwkedH4WmXhdIPKT6l6oOxd1PUbNodGI6DcBekgN7EC//+x3pfBYX/qL1gNmZAqivw5g==",
"cpu": [
"arm64"
],
@@ -609,9 +669,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.18.tgz",
"integrity": "sha512-XuDHFzbFEtZl0QGJi92+fmG67tfnp1iZ9VOeDkwasGyTq2VG3/5PnJnvAr+MTzFh/rNScJ3VhN+XpZyyGsN7Jw==",
"version": "1.2.19",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.19.tgz",
"integrity": "sha512-zo8EsSSwMZCY/e+fRnGaX02YGgvJD0yvhNi/9hpGPA2MJ9ZF8dPCw75UGfpI6OpQ/myR/dIYmogRvqiZD0TrXg==",
"cpu": [
"x64"
],
@@ -627,9 +687,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.18.tgz",
"integrity": "sha512-MM8opLfsLK15oEXDz6iBc0t3ahu+ew9jsAmha+ovZ33hSwYgqo7YmpRITqFTfW7xA71BLEQ3jJdTsaCmhB1c9w==",
"version": "1.2.19",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.19.tgz",
"integrity": "sha512-O8ISAOfI1nj7AuBPd30FSF47z7f7lzlk5poQVGgiikvQuGEBFYvW8ki+UEnT+YJPnNGjQzn304klt4O9FUE7mA==",
"cpu": [
"x64"
],
@@ -645,9 +705,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.18.tgz",
"integrity": "sha512-J6EoCWiy5RnFQzHrKyHXXevZ9lukQfGpnchX/zG/k/WC2nc9O9XyS1JgA8kI9VxMFggMbCPjpv7LcikriXfehw==",
"version": "1.2.19",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.19.tgz",
"integrity": "sha512-/lYCba5NO0yq4S0jGMH8QTBtkvQxoXjoQWzF5BcWp9MjB9jxVRD1GQDNofkVjcOj30DWyhLIN/4Oe60JKzaONw==",
"cpu": [
"arm"
],
@@ -663,9 +723,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.18.tgz",
"integrity": "sha512-HIkFuLF7CdDeki+1aCZLLbLq0hk/PHntb+z4/YCTo47JbksTD6jJEELIz92tYROmDmvMrNqUMqBi71BkJLDR+A==",
"version": "1.2.19",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.19.tgz",
"integrity": "sha512-pm/apFMBnSbFb3O6nG5pHO1iqHhZrjGDFEjSm+ot9J17XXPvH9L90y5h+jgfHCE+Qdlsvy8qKEcU4VKxgjD+vA==",
"cpu": [
"arm64"
],
@@ -684,9 +744,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.18.tgz",
"integrity": "sha512-4oqWItLLYt7IceOnElCct8T2ek5p7PmD7LN04TRJRXGo5f3HtKTgGH6PhYRFK4C2ICkq/LMUU0BVPvvlAKhm8g==",
"version": "1.2.19",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.19.tgz",
"integrity": "sha512-K1SmQp3FFichn8oqrNIwvFoZ5wcvjC7goBzpaMNvLf9FZc+QB/QvLmnpJZgAAGHiD980tP8FZZnXk2K94rvlxg==",
"cpu": [
"arm64"
],
@@ -705,9 +765,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.18.tgz",
"integrity": "sha512-+AkRly5SrCEgnPhGRfub7pucASYEC2fTsxZg7VCcnozpA2+Wn/gCYy1t7LuJS67/xyJu/7noYBJncDUkbwgw2A==",
"version": "1.2.19",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.19.tgz",
"integrity": "sha512-I0xwD57jP/3A7+5UPZin2w8Y7XHEmEM5zqpYZOcQ45MlNW6tUJUziymV3yRr7rL4vwyfEcRsRksC4a7Kc08p5Q==",
"cpu": [
"ppc64"
],
@@ -726,9 +786,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.18.tgz",
"integrity": "sha512-Ttg1r9vVykMnQrE/GwYO9icKtXiuiMfRe/w74xJkeoVuvhRG+WtT6LEMOf58Yl63yd77lhHzO8Y55R8CC3XALA==",
"version": "1.2.19",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.19.tgz",
"integrity": "sha512-BrtEHT24SNXj9HiXqy+s/kXiSiQGt6EKCPUlRirAoIioVxKGnbXBUEkbZASAei7iZJB0/Ynm+0JP00Ipx7AnSw==",
"cpu": [
"s390x"
],
@@ -747,9 +807,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.18.tgz",
"integrity": "sha512-xX1Tt4o5jUMma0vJsXp5eRZA9nOuyigb2dKJMMzZRmzMxLYGE9PaKKSqHPzPvSQgM8ZOEacSTQwDQcwWfh0xmQ==",
"version": "1.2.19",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.19.tgz",
"integrity": "sha512-rm75/9DkHKeU2WnA8w4X94WGYgLlDCFCfkQC+4bguVJmC/jvDRv2B+2wLOYhyE0fVuv7//Ckk39OoOufyCu76Q==",
"cpu": [
"x64"
],
@@ -768,9 +828,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.18.tgz",
"integrity": "sha512-4VVlGI5K2kociZlW3lSN8VjG3TW5yQIlAcfCdBXMgGjJxZll15Fa4ocRHXHvIVVnwlibCw/NQajJrNDiqqpiiw==",
"version": "1.2.19",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.19.tgz",
"integrity": "sha512-irh/4+iYFM4GAzpluDErgjz89bj6NDtqqMAG/6O1LG38Ed3NpyebzCpcVNkFW8uhy+/3/BocUvnXDR33CXWe9A==",
"cpu": [
"x64"
],
@@ -789,9 +849,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.18.tgz",
"integrity": "sha512-PApTsgH1jLYuyy3tSCz9De82Gq5qXwi7vImVZhEzTgX1C4150Cp5M2ZfL3pLvbCvbEHL0oJeC6pd6podtv8Y5w==",
"version": "1.2.19",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.19.tgz",
"integrity": "sha512-+KWTWNoPt+CfSq94CiceWu3yPE+QzkaG+phPyz9G60tmZLwfQkcA4AeksafzvJAw4pvg1mcd5a1TkUqVv0s4JA==",
"cpu": [
"arm64"
],
@@ -806,10 +866,30 @@
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.2.19",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.2.19.tgz",
"integrity": "sha512-7EH+skOsl5qpWnK0vzamgJFhOv1cwxQpsxm8tI883K9fur5c27p8q87fzEO6BbBPabA2/+s/zR63/pLc137iOg==",
"cpu": [
"wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@emnapi/core": "2.0.0-beta.2",
"@emnapi/runtime": "2.0.0-beta.2",
"@napi-rs/wasm-runtime": "1.2.6"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=23.5.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.18.tgz",
"integrity": "sha512-J2XjWjx/SP+4x0H4r6dz3Vzb5WVkg7re40Do9K7HJ3sK2/xcmZrWwakXlSwaMvOkvruLJCD4CL/3wqsIR/thgg==",
"version": "1.2.19",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.19.tgz",
"integrity": "sha512-3Q1Yg1Q7tEvBWoZf5rFvG/GKZQPgwfru+p4wBajDfJFBs0SHCpL++dfTjW9gyAKd1T7ZpIYvdaUax7Pa/IhFVQ==",
"cpu": [
"arm64"
],
@@ -825,9 +905,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.18.tgz",
"integrity": "sha512-XwRJxRQvkuEtS0hIIt29QrqPJ+JzMJSNYHJw4ae/mc6uEarfRG1ceyg4HmmWiBdL/YCb6g7N5BLbSvmRJRYZHw==",
"version": "1.2.19",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.19.tgz",
"integrity": "sha512-y/sqRRc6d7hECzOFc5n3LSeqctmIp9mMjY1Kq4s14uLPd/LZ4z7i1zrW6fMtp2mPeuQIC4e6ULMcwQzMomqQag==",
"cpu": [
"x64"
],
@@ -843,9 +923,9 @@
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.1.0.tgz",
"integrity": "sha512-K+NU2HDCEem4sClgHH4ZZXwdJeNFkJo+hRk479DIwdIviAPN2hFJH1KHHwg8CxHQXvi0M5XY4deTXINfIiSG6w==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.1.1.tgz",
"integrity": "sha512-glFIJDvLNDtJhT9qrMFWLPbvS+skxgt4L0K8Ei2YtnhPysAJnsI7nyxSJxKj4ADeKX0OLjC4ntRkwpLOoS3rwA==",
"dev": true,
"license": "MIT",
"peer": true
@@ -857,12 +937,24 @@
"license": "MIT"
},
"node_modules/@tsconfig/node22": {
"version": "22.0.6",
"resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.6.tgz",
"integrity": "sha512-/5thavHAnWDZwH2H6eEn/T8pBEBhtuKaWGMslsj82kJfvzQgOgEMK7X4goBbIUjgVtAzPTtM9Ml64LA0uxO6Iw==",
"version": "22.0.7",
"resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.7.tgz",
"integrity": "sha512-xnYM5q+HTeLm+jRBIB7Mu61OyOmNpgzPmJuhwPODSyITDPIWVK2TXxn/+O5fJ4n/P0u/gPsgSyPw/Dl3Cz/L4g==",
"dev": true,
"license": "MIT"
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz",
"integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@types/chai": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
@@ -1313,9 +1405,9 @@
}
},
"node_modules/brace-expansion": {
"version": "5.0.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
"integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
"version": "5.0.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.13.tgz",
"integrity": "sha512-Vh3dmFbc4wZeox7M9I/0L9GfFd83Su4+yCck/OF9Gi/P/dAHn0RT7X4K9R7gMNMhDck3DvhR6QXsnQdjjgwNvQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1543,9 +1635,9 @@
"peer": true
},
"node_modules/encodeurl": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.1.tgz",
"integrity": "sha512-Ev/hIyePLZtpL98x562lvPtenrUfGr0lfaf44HFZ+hDEwso7ccV9QAZrEVLg0hxNAzygaAFxy2dYaAKKM0G06A==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.3.tgz",
"integrity": "sha512-mIteYONiEfc4xWf2F1WXovh1c8npzS00vNeiUPdwFF+QlVJni+gNvpQltLMQhob8rew/bWWc/bGTuUr0EUGt0w==",
"license": "MIT",
"peer": true,
"engines": {
@@ -2270,9 +2362,9 @@
}
},
"node_modules/hono": {
"version": "4.13.17",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.17.tgz",
"integrity": "sha512-vZIcbkvjOR5hHRWERbGkr8fGfF0GGM1ooYkuH448iO3b2AdVEr3AXZSw4Z3Kn+1L5z0CVoVeyV29VGYmFh05kQ==",
"version": "4.13.18",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.18.tgz",
"integrity": "sha512-H9q/E1XizaWstJxUW881dmLxEj+t/TrGGnyxINkm4+sK0qUwsMtFdo2BWI9Vs3cIS19MU6b6iCijoUoEf4f3DA==",
"license": "MIT",
"peer": true,
"engines": {
@@ -3155,9 +3247,9 @@
}
},
"node_modules/prettier": {
"version": "3.9.10",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz",
"integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==",
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.10.1.tgz",
"integrity": "sha512-70vp3ZUYoPlFbF9uiTdXHUUCGEIp7d4+6p+8DYbkm2jP7x/3i/paXVF6dopSI3zd/CEitrZN1rc4i85xWmmXlg==",
"dev": true,
"license": "MIT",
"bin": {
@@ -3275,9 +3367,9 @@
}
},
"node_modules/rolldown": {
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.18.tgz",
"integrity": "sha512-C1uuT3UR60SShuCaPQFAAZyefJCWZlAKB02BV3BuPE2Rs4fevRQltEKoFO2tV5W9/eJhU5RieV6vJj3euulfNQ==",
"version": "1.2.19",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.19.tgz",
"integrity": "sha512-QIiV1J9d0pzN4jeeTH4sjPy0AZ6cAy2cG0ExeoE1+U8Rvj+hR3gYkokx0YbeUNGlZqiQJJ2utcETe0oKXwzsuA==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -3292,21 +3384,22 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm-eabi": "1.2.18",
"@rolldown/binding-android-arm64": "1.2.18",
"@rolldown/binding-darwin-arm64": "1.2.18",
"@rolldown/binding-darwin-x64": "1.2.18",
"@rolldown/binding-freebsd-x64": "1.2.18",
"@rolldown/binding-linux-arm-gnueabihf": "1.2.18",
"@rolldown/binding-linux-arm64-gnu": "1.2.18",
"@rolldown/binding-linux-arm64-musl": "1.2.18",
"@rolldown/binding-linux-ppc64-gnu": "1.2.18",
"@rolldown/binding-linux-s390x-gnu": "1.2.18",
"@rolldown/binding-linux-x64-gnu": "1.2.18",
"@rolldown/binding-linux-x64-musl": "1.2.18",
"@rolldown/binding-openharmony-arm64": "1.2.18",
"@rolldown/binding-win32-arm64-msvc": "1.2.18",
"@rolldown/binding-win32-x64-msvc": "1.2.18"
"@rolldown/binding-android-arm-eabi": "1.2.19",
"@rolldown/binding-android-arm64": "1.2.19",
"@rolldown/binding-darwin-arm64": "1.2.19",
"@rolldown/binding-darwin-x64": "1.2.19",
"@rolldown/binding-freebsd-x64": "1.2.19",
"@rolldown/binding-linux-arm-gnueabihf": "1.2.19",
"@rolldown/binding-linux-arm64-gnu": "1.2.19",
"@rolldown/binding-linux-arm64-musl": "1.2.19",
"@rolldown/binding-linux-ppc64-gnu": "1.2.19",
"@rolldown/binding-linux-s390x-gnu": "1.2.19",
"@rolldown/binding-linux-x64-gnu": "1.2.19",
"@rolldown/binding-linux-x64-musl": "1.2.19",
"@rolldown/binding-openharmony-arm64": "1.2.19",
"@rolldown/binding-wasm32-wasi": "1.2.19",
"@rolldown/binding-win32-arm64-msvc": "1.2.19",
"@rolldown/binding-win32-x64-msvc": "1.2.19"
}
},
"node_modules/router": {
@@ -3601,6 +3694,15 @@
"typescript": ">=4.8.4"
}
},
"node_modules/tslib": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.9.1.tgz",
"integrity": "sha512-a4Ux9VKuIMHsOS7ENbEq6hK2ZGYIT27Rtl0E4eD9KqwxhKiQeMZE/9xb3IdYwWaSOyIBQLExhQgXDTYoiCxnWA==",
"dev": true,
"license": "0BSD",
"optional": true,
"peer": true
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -3699,9 +3801,9 @@
}
},
"node_modules/vite": {
"version": "8.3.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.7.tgz",
"integrity": "sha512-oxh0dMxrayBr33ScJvvBJ2HBQ2jc1TBrXKR2NyL/MJ9WPuX19mN2DXWUmywKdyw5RFVQBo8hmtb2FMC0LHrnng==",
"version": "8.3.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.8.tgz",
"integrity": "sha512-LzOn3h9rG/0vhLdXrV4yJ4QFA9eEoqCl+Yx+kGJd7aB9w6nfNgVHKSYwLPZUHrz4ZGa+t+kXjV9itvcHDLQouA==",
"dev": true,
"license": "MIT",
"peer": true,

View File

@@ -3,7 +3,7 @@
"publishConfig": {
"access": "public"
},
"version": "0.104.1",
"version": "0.107.0",
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
"main": "dist/lib.js",
"types": "dist/lib.d.ts",
@@ -68,15 +68,15 @@
"node": ">=22"
},
"dependencies": {
"@agentclientprotocol/sdk": "2.5.0",
"@anthropic-ai/claude-agent-sdk": "0.3.322",
"@agentclientprotocol/sdk": "2.6.0",
"@anthropic-ai/claude-agent-sdk": "0.3.323",
"diff": "9.0.0",
"zod": "4.6.7"
},
"devDependencies": {
"@anthropic-ai/sdk": "1.7.0",
"@anthropic-ai/sdk": "1.8.0",
"@eslint/js": "10.0.1",
"@tsconfig/node22": "22.0.6",
"@tsconfig/node22": "22.0.7",
"@types/node": "26.6.6",
"@typescript-eslint/eslint-plugin": "8.72.2",
"@typescript-eslint/parser": "8.72.2",
@@ -85,7 +85,7 @@
"eslint": "10.14.0",
"eslint-config-prettier": "10.1.8",
"globals": "17.14.0",
"prettier": "3.9.10",
"prettier": "3.10.1",
"typescript": "6.0.3",
"vitest": "5.1.0"
}

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

@@ -879,7 +879,7 @@ function recordPublished(task: AsyncTask, fields: Record<string, unknown>): void
* its tool result. The text of a tool result never gives a task id: a
* foreground command can print any text.
*
* Claude Code sends no `tool_use_result` with a subagent's tool results. For
* Claude Code sends no `tool_use_result` with a subagent's Bash results. For
* those, `startedTaskOf` gives the background `local_bash` task whose
* `task_started` named the Bash call, which comes before the result.
*/

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

@@ -5,7 +5,7 @@
* `getContextUsage`, and learned from each result's `modelUsage`.
*/
import type { ModelInfo, Query } from "@anthropic-ai/claude-agent-sdk";
import { DEFAULT_CONTEXT_WINDOW, inferContextWindowFromModel } from "./session-usage.js";
import { DEFAULT_CONTEXT_WINDOW } from "./session-usage.js";
/** Cross-session cache of authoritative context windows, keyed by
* `${providerCacheKey}\0${modelId}` (see {@link contextWindowCacheKey}).
@@ -61,8 +61,8 @@ const PROVIDER_ROUTING_ENV_VARS = [
* Header/credential values can be secrets; the key only ever lives as an
* in-memory Map key and is never logged or surfaced. Over-keying is the safe
* side: a var change that didn't really change the backend costs one cache
* miss (heuristic seed until the next result), while under-keying would serve
* one backend's window for another's. */
* miss (the default window until Claude Code reports it), while under-keying
* would serve one backend's window for another's. */
export function providerCacheKeyFor(env: Record<string, string | undefined>): string {
return PROVIDER_ROUTING_ENV_VARS.map((name) => env[name] ?? "").join("\0");
}
@@ -107,31 +107,24 @@ export function cacheResultContextWindow(
/** The context window to report *right now* for a model, with NO IPC on the
* critical path: the cached authoritative value if we've learned it (from a
* prior turn's `result.modelUsage`, this or any session on the same backend),
* else the text heuristic over the model row's identity strings, else the
* default. Derives the cache key itself — `modelInfo?.resolvedModel ?? modelId`,
* the same rule at every seed site — so read keys can't drift from the write
* site's spelling. `authoritative` reports whether the value came from the
* cache: an authoritative window can legitimately equal
* DEFAULT_CONTEXT_WINDOW, so the value alone can't tell the caller. */
* else DEFAULT_CONTEXT_WINDOW. The model's names do not give the window:
* Claude Code 2.1.323 reports 1M for `sonnet`, and 200K for it under
* `CLAUDE_CODE_DISABLE_1M_CONTEXT=1`. Derives the cache key itself —
* `modelInfo?.resolvedModel ?? modelId`, the same rule at every seed site — so
* read keys can't drift from the write site's spelling. `authoritative`
* reports whether the value came from the cache: an authoritative window can
* legitimately equal DEFAULT_CONTEXT_WINDOW, so the value alone can't tell the
* caller. */
export function immediateContextWindow(
providerCacheKey: string,
modelId: string,
modelInfo?: Pick<ModelInfo, "resolvedModel" | "displayName" | "description">,
modelInfo?: Pick<ModelInfo, "resolvedModel">,
): { size: number; authoritative: boolean } {
const cached = contextWindowCache.get(
contextWindowCacheKey(providerCacheKey, modelInfo?.resolvedModel ?? modelId),
);
if (cached !== undefined) return { size: cached, authoritative: true };
return {
size:
inferContextWindowFromModel(
modelId,
modelInfo?.resolvedModel,
modelInfo?.displayName,
modelInfo?.description,
) ?? DEFAULT_CONTEXT_WINDOW,
authoritative: false,
};
return { size: DEFAULT_CONTEXT_WINDOW, authoritative: false };
}
/** Forgets the learned windows, as a logout asks: 1M-context entitlement is

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

@@ -1,5 +1,28 @@
import { resolveSettings } from "@anthropic-ai/claude-agent-sdk";
/** The machine policy sources that `machinePolicy.didNotLoad` names. */
const POLICY_SOURCES: Record<string, string> = {
profile: "the macOS MDM profile",
registry: "the Windows registry policy",
file: "the managed-settings file",
};
/**
* The machine policy sources that may be there and did not load
* (`machinePolicy.didNotLoad`, SDK 0.3.323), named for a log line: "the macOS
* MDM profile, the managed-settings file". The resolved settings show such a
* source as absent, so the adapter applies nothing from it, and Claude Code
* reads the policy itself. Undefined when every source loaded, and when the
* SDK does not report it.
*/
export function unloadedPolicySources(
resolved: Pick<Awaited<ReturnType<typeof resolveSettings>>, "machinePolicy">,
): string | undefined {
const unloaded = resolved.machinePolicy?.didNotLoad;
if (!unloaded || unloaded.length === 0) return undefined;
return unloaded.map((kind) => POLICY_SOURCES[kind] ?? kind).join(", ");
}
/**
* Applies the managed-policy tier's env vars to `process.env` before any SDK
* call, so the SDK subprocess inherits them. Going through `resolveSettings`
@@ -28,6 +51,13 @@ export async function applyManagedPolicyEnv(
console.error("Failed to resolve managed policy settings:", error);
return;
}
const unloaded = unloadedPolicySources(policy);
if (unloaded) {
console.error(
`Managed settings did not load from ${unloaded}: its env vars are not applied. ` +
"Restart the agent to read it again.",
);
}
for (const [key, value] of Object.entries(policy.effective.env ?? {})) {
env[key] = value;
}

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

@@ -117,10 +117,15 @@ export type TaskEnd = {
/** The SDK's reason for an unusual end, such as `worker_restart`. */
reason?: string;
/**
* The adapter knows that the work ended, but not why: the stream ended, or
* the conversation was reset, with the task still running.
* The adapter knows that the work ended, but not why: the stream ended
* with the task still running.
*/
unexplained?: boolean;
/**
* The adapter stops following the task, which may still run: the
* conversation was reset, and the runtime forgets its children.
*/
unobservable?: boolean;
};
type TaskStarted = {
@@ -225,6 +230,11 @@ export class NativeSubagentRuntime {
string,
{ initial: AcpSessionNotification; message?: string; delegationId?: string }
>();
/**
* The RFD form: the agent that each TaskStop or SendMessage call names (see
* {@link targetAgentId}), until the call's final update.
*/
private readonly targetByToolCall = new Map<string, string>();
private readonly generationByTaskId = new Map<string, number>();
private readonly pending = new Map<string, AcpSessionNotification[]>();
private pendingCount = 0;
@@ -280,7 +290,10 @@ export class NativeSubagentRuntime {
const routed = await this.routeUpdate(notification, deliver, forcedSessionId);
if (!routed) return routed;
this.rememberSendMessage(routed);
return this.form === "rfd" ? this.routeRfdSendMessage(routed, forcedSessionId) : routed;
if (this.form !== "rfd") return routed;
this.rememberTarget(routed);
const shown = this.routeRfdSendMessage(routed, forcedSessionId);
return shown && this.withTarget(shown);
}
/**
@@ -828,7 +841,10 @@ export class NativeSubagentRuntime {
if (child.parentToolUseId) {
for (const pending of this.takePending(child.parentToolUseId)) await deliver(pending);
}
if (this.form === "rfd") {
if (this.form === "rfd" && end?.unobservable) {
// The child's `cancel` goes with it: a forgotten child cannot be stopped.
await this.reportState(child, { state: "unknown" }, { capabilities: null });
} else if (this.form === "rfd") {
await this.reportState(child, rfdEndState(state, end));
child.missingError = state === "failed" && !end?.error && !end?.unexplained;
if (child.parentToolUseId) (child.endedRuns ??= new Set()).add(child.parentToolUseId);
@@ -853,14 +869,33 @@ export class NativeSubagentRuntime {
/**
* Ends every child that still runs, because the stream ended or the
* conversation was reset. AIR's draft reports `state` for each. In the RFD
* form the cause of each child's end is unknown, so it is idle with no stop
* reason: a child that a cancel stopped already reported it from the SDK's
* events, and a background child that outlived the cancel was not cancelled.
* conversation was reset. AIR's draft reports `state` for each.
*
* The RFD form, at the end of the stream: Claude Code's process ran the
* children and is gone, or the caller closes the query next, which
* terminates it, so their work ended. Its cause is unknown, so each is idle with no stop reason: a
* child that a cancel stopped already reported it from the SDK's events,
* and a background child that outlived the cancel was not cancelled.
*
* The RFD form, at a conversation reset (`mayStillRun`): Claude Code can
* keep the old conversation's subagents running, and the caller forgets
* them next (`clear`), so no later event of theirs is reported. Their work
* did not end as far as the adapter knows, so each is `unknown`, as the
* RFD requires when a child can no longer be observed, and loses its
* `cancel` capability. A child that is idle keeps its state.
*/
async finishAll(state: SubagentState, deliver: Publish): Promise<void> {
async finishAll(
state: SubagentState,
deliver: Publish,
{ mayStillRun = false }: { mayStillRun?: boolean } = {},
): Promise<void> {
const errors: unknown[] = [];
const end = this.form === "rfd" ? { unexplained: true } : undefined;
const end: TaskEnd | undefined =
this.form !== "rfd"
? undefined
: mayStillRun
? { unobservable: true }
: { unexplained: true };
try {
for (const taskId of [...this.children.keys()].reverse()) {
try {
@@ -1235,13 +1270,25 @@ export class NativeSubagentRuntime {
await this.publishConfiguration(child);
}
/** The RFD form: publishes the work state of `child` on its parent. */
private async reportState(child: NativeSubagent, state: WorkState): Promise<void> {
/**
* The RFD form: publishes the work state of `child` on its parent, with
* `capabilities` when the child's change with it.
*/
private async reportState(
child: NativeSubagent,
state: WorkState,
capabilities?: { capabilities: null },
): Promise<void> {
child.workState = state.state;
child.missingError = false;
await this.publish({
sessionId: child.parentSessionId,
update: { sessionUpdate: "subagent_update", sessionId: child.sessionId, state },
update: {
sessionUpdate: "subagent_update",
sessionId: child.sessionId,
...capabilities,
state,
},
});
}
@@ -1439,6 +1486,42 @@ export class NativeSubagentRuntime {
return unexposedControlFallback(sendMessage.initial, notification, sendMessage.sender);
}
/** The RFD form: remembers the agent that a TaskStop or SendMessage call names. */
private rememberTarget({ update }: AcpSessionNotification): void {
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") return;
const claudeMeta = update._meta?.claudeCode as { toolName?: unknown } | undefined;
const agentId = targetAgentId(claudeMeta?.toolName, update.rawInput);
if (agentId !== undefined) this.targetByToolCall.set(update.toolCallId, agentId);
}
/**
* The RFD form: `notification`, an update that the client is shown, with
* the RFD's `session` reference when it is of a tool call that operates on
* an announced child of another session (see {@link withSessionReference}):
*
* - a TaskStop or SendMessage call that names the child by its agent id;
* - a SendMessage call that resumed the child, and stays shown because a
* permission request showed it;
* - an Agent/Task call that launched the child, and stays shown for the
* same reason.
*
* The RFD requires the announcement before the reference, so a call whose
* child is not announced yet gets none.
*/
private withTarget(notification: AcpSessionNotification): AcpSessionNotification {
const { update } = notification;
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
return notification;
}
const agentId = this.targetByToolCall.get(update.toolCallId);
if (isFinalToolCallStatus(update.status)) this.targetByToolCall.delete(update.toolCallId);
const child =
this.childByParentToolUse.get(update.toolCallId) ??
(agentId !== undefined ? this.children.get(agentId) : undefined);
if (!child?.announced || child.sessionId === notification.sessionId) return notification;
return withSessionReference(notification, child.sessionId);
}
/** Whether `update` is of an Agent/Task call, by its tool name or, in the
* RFD form, by the id of a call the client is not shown. */
private isControlUpdate(
@@ -1504,6 +1587,7 @@ export class NativeSubagentRuntime {
this.hiddenLaunches.clear();
this.hiddenSendMessages.clear();
this.handbacks.clear();
this.targetByToolCall.clear();
this.generationByTaskId.clear();
this.pending.clear();
this.pendingCount = 0;
@@ -1835,7 +1919,7 @@ async function publishPrompt(
* prompt goes from the parent to the child, a SendMessage call, whose message
* goes from its sender to a child, or a child's SubagentHandback call, whose
* report goes from the child to the parent. Both views use the
* call's tool use id, and each names the other with the draft RFD's
* call's tool use id, and each names the other with the RFD's
* `recipientMessageId` and `senderMessageId`.
*/
export function linkedSessionMessages(
@@ -1863,6 +1947,49 @@ export function linkedSessionMessages(
];
}
/**
* The RFD form: the agent that a tool call names as the one it operates on:
* the `task_id` of a TaskStop call, or the `to` of a SendMessage call. It is
* an agent only if it is the id of one: an agent's id is its child session's
* id. Both tools also take a name, which is not resolved here: Claude Code
* resolves it, and the adapter cannot tell which agent it picked. `undefined`
* for any other call.
*/
export function targetAgentId(toolName: unknown, rawInput: unknown): string | undefined {
if (typeof rawInput !== "object" || rawInput === null) return undefined;
const input = rawInput as { task_id?: unknown; to?: unknown };
if (toolName === "TaskStop") return nonBlankString(input.task_id);
if (toolName === "SendMessage") return nonBlankString(input.to);
return undefined;
}
/**
* The RFD form: `notification`, a tool call's report, with a `session`
* reference to `sessionId` after its content: `{ "type": "session",
* "sessionId": "..." }`, which links the call to the session it operates on.
* A `tool_call_update` replaces the content that the client holds, so the
* reference goes on every report that carries content, and a
* `tool_call_update` without content is returned as it is.
*/
export function withSessionReference(
notification: AcpSessionNotification,
sessionId: string,
): AcpSessionNotification {
const { update } = notification;
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
return notification;
}
if (update.sessionUpdate === "tool_call_update" && update.content == null) return notification;
const content = update.content ?? [];
if (content.some((item) => item.type === "session" && item.sessionId === sessionId)) {
return notification;
}
return {
...notification,
update: { ...update, content: [...content, { type: "session", sessionId }] },
};
}
/**
* The tool with which a child hands its report to its parent, when Claude
* Code runs in hand-back mode. The Agent/Task result then only says that the
@@ -1953,6 +2080,9 @@ function isWorking(child: NativeSubagent): boolean {
* A task that a worker restart orphaned, or that was still running when the
* stream ended, is idle with no stop reason: its work ended, but the adapter
* did not see a cancellation or a failure, which the RFD forbids claiming.
*
* A child that the adapter can no longer observe and that may still run is
* not idle but `unknown` (see `NativeSubagentRuntime.finishAll`).
*/
export function rfdEndState(state: SubagentState, end: TaskEnd | undefined): WorkState {
if (end?.unexplained || end?.reason === "worker_restart") return { state: "idle" };

View File

@@ -5,3 +5,16 @@ import * as path from "node:path";
export function claudeConfigDir(): string {
return process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
}
/** `<config>/projects`, normalized like the SDK does. */
export function projectsRoot(): string {
return path.join(claudeConfigDir(), "projects").normalize("NFC");
}
const SESSION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
/** Whether `value` is a Claude session id (a UUID). Only such ids name a
* transcript, and only such ids are safe to use as a file name. */
export function isSessionId(value: unknown): value is string {
return typeof value === "string" && SESSION_ID_PATTERN.test(value);
}

View File

@@ -8,9 +8,26 @@
import type {
SDKMessage,
SDKMessageOrigin,
SDKRateLimitInfo,
SDKResultMessage,
} from "@anthropic-ai/claude-agent-sdk";
/**
* The `data` of the error that fails a turn, for clients to dispatch on
* without matching its text: the assistant message's `errorKind`, and for a
* `rate_limit` failure the turn's latest `rate_limit_event` info as
* `rateLimit`, whose `resetsAt` says when the account can run again. A turn
* refused before its first usage sends no `usage_update` to carry that info.
* `undefined` without an error kind.
*/
export function turnErrorData<K extends string>(
errorKind: K | undefined,
rateLimit: SDKRateLimitInfo | undefined,
): { errorKind: K; rateLimit?: SDKRateLimitInfo } | undefined {
if (!errorKind) return undefined;
return errorKind === "rate_limit" && rateLimit ? { errorKind, rateLimit } : { errorKind };
}
export type ResultEnd =
| { stopReason: "refusal" | "max_tokens" | "end_turn" | "max_turn_requests" }
/**

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,8 @@ import {
type LiveSessionRegistry,
type LiveSnapshot,
} from "./live-registry.js";
import { isExactProjectDir, isSessionId, projectDirMatches, projectsRoot } from "./project-dirs.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;

View File

@@ -14,24 +14,10 @@
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { claudeConfigDir } from "../../paths.js";
/** The SDK cuts an encoded path at this length and appends a hash. */
const MAX_SANITIZED_LENGTH = 200;
const SESSION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
/** Whether `value` is a Claude session id (a UUID). Only such ids name a
* transcript, and only such ids are safe to use as a file name. */
export function isSessionId(value: unknown): value is string {
return typeof value === "string" && SESSION_ID_PATTERN.test(value);
}
/** `<config>/projects`, normalized like the SDK does. */
export function projectsRoot(): string {
return path.join(claudeConfigDir(), "projects").normalize("NFC");
}
function sanitize(value: string): string {
return value.replace(/[^a-zA-Z0-9]/g, "-");
}

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,11 +36,11 @@ import {
type ListSubscriptionDeps,
} from "./list-subscriptions.js";
import { LiveSessionRegistry } from "./live-registry.js";
import { isSessionId } from "../paths.js";
import {
canonicalPath,
exists,
isExactProjectDir,
isSessionId,
projectDirMatches,
sameProjectPath,
} from "./project-dirs.js";
@@ -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,20 +21,19 @@
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 {
canonicalPath,
encodeProjectPath,
errorCode,
exists,
isExactProjectDir,
isSessionId,
normalizePath,
pathAndAncestors,
projectDirMatches,
projectDirsOf,
projectsRoot,
sameProjectPath,
} from "./project-dirs.js";
import {

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

@@ -550,7 +550,55 @@ function stringFields(text: string, key: string): string[] {
return values;
}
const lastField = (text: string, key: string) => stringFields(text, key).at(-1) || undefined;
/** The string `field` of the JSON record `line`, which is of `type` when one
* is given. `null` when the line is no JSON. */
function recordField(line: string, field: string, type?: string): string | null | undefined {
let record: unknown;
try {
record = JSON.parse(line);
} catch {
return null;
}
if (typeof record !== "object" || record === null) return undefined;
if (type !== undefined && (record as Entry).type !== type) return undefined;
const value = (record as Entry)[field];
return typeof value === "string" ? value : undefined;
}
/**
* The top-level string `field` of the last record of `text` that has one,
* as the SDK reads a session's metadata since 0.3.323: it parses each line
* that names the field, from the last one back. A `"customTitle":"…"` inside
* a message, such as a tool result that shows a transcript, is no title.
*
* With `type`, only a record of that `type` counts, and a line that names
* the type but is no JSON is read from its last `{"type":"<type>"` that does
* not follow `:`, `,` or `[`: a record appended to a line whose write was cut
* off. The record that the start of the tail window cuts has no such start
* in the window, and is not read.
*
* Undefined when the field of that record is empty: older records are not
* searched.
*/
function lastRecordField(text: string, field: string, type?: string): string | undefined {
const fieldMarker = `"${field}":`;
const typeMarkers = type === undefined ? undefined : [`"type":"${type}"`, `"type": "${type}"`];
const lines = text.split("\n");
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i]!;
if (!line.includes(fieldMarker)) continue;
if (typeMarkers && !typeMarkers.some((marker) => line.includes(marker))) continue;
let value = recordField(line, field, type);
if (value === null && typeMarkers) {
const start = line.lastIndexOf(`{${typeMarkers[0]}`);
if (start > 0 && !":,[".includes(line[start - 1]!)) {
value = recordField(line.slice(start), field, type);
}
}
if (typeof value === "string") return value || undefined;
}
return undefined;
}
/** Whether the first record of the transcript is a sidechain one, which the
* SDK never lists. */
@@ -560,9 +608,10 @@ export function isSidechainTranscript(head: string): boolean {
return first.includes('"isSidechain":true') || first.includes('"isSidechain": true');
}
/** The last `customTitle` of the tail, as the SDK reads it. */
/** The title of the last `custom-title` record of the tail, as the SDK reads
* it (see {@link lastRecordField}). */
export function tailCustomTitle({ tail, sdkTail }: HeadTail): string | undefined {
return lastField(sdkTail ?? tail, "customTitle");
return lastRecordField(sdkTail ?? tail, "customTitle", "custom-title");
}
/** The last non-blank top-level `agentName` of the records of `text`, the
@@ -607,22 +656,23 @@ export function sdkTitles(
): SdkTitles {
const tail = sdkTail ?? wholeLines;
const customTitle =
lastField(tail, "customTitle") ??
lastRecordField(tail, "customTitle", "custom-title") ??
sidecarTitle ??
lastField(head, "customTitle") ??
lastField(tail, "aiTitle") ??
lastField(head, "aiTitle");
lastRecordField(head, "customTitle", "custom-title") ??
lastRecordField(tail, "aiTitle", "ai-title") ??
lastRecordField(head, "aiTitle", "ai-title");
return { customTitle, summary: customTitle ?? generatedTitle({ head, tail }) };
}
/** The title of a transcript without a custom title, in the SDK's order: AI
* title, last prompt, summary, first prompt. */
* title, last prompt, summary, first prompt. The last prompt and the summary
* are the field of a record of any type. */
export function generatedTitle({ head, tail }: HeadTail): string | undefined {
return (
lastField(tail, "aiTitle") ??
lastField(head, "aiTitle") ??
lastField(tail, "lastPrompt") ??
lastField(tail, "summary") ??
lastRecordField(tail, "aiTitle", "ai-title") ??
lastRecordField(head, "aiTitle", "ai-title") ??
lastRecordField(tail, "lastPrompt") ??
lastRecordField(tail, "summary") ??
(firstPrompt(head) || mediaPrompt(head) || undefined)
);
}

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

@@ -1,5 +1,11 @@
import type { SessionConfigOption } from "@agentclientprotocol/sdk/unstable";
import type { ModelInfo, Query, Settings } from "@anthropic-ai/claude-agent-sdk";
// matchDeniedModels is marked @alpha in the SDK; API may shift in a future release.
import {
matchDeniedModels,
type ModelInfo,
type Query,
type Settings,
} from "@anthropic-ai/claude-agent-sdk";
import { AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY, withAirMeta } from "./air-extension.js";
import { bedrockRegionPrefix } from "./gateway-models.js";
@@ -105,7 +111,7 @@ function containsWords(haystack: string, needle: string): boolean {
}
/** `id` without the gateway spellings that `normalizeModelId` keeps for
* `deniedModels` and gateway listings: path segments before `claude-`
* gateway listings: path segments before `claude-`
* (`vertex_ai/`, `openrouter/anthropic/`, `publishers/anthropic/models/`)
* and the Anthropic API's `-latest` suffix (`claude-3-5-haiku-latest`). */
function withoutGatewaySpelling(id: string): string {
@@ -376,8 +382,8 @@ export function aliasResumedModelRow(
/** A model id without the spellings that name the same model: provider
* prefixes, dates, version suffixes, `:variant` suffixes, dotted versions,
* `-fast` and context hints. `deniedModels` and gateway listings both match on
* it. OpenRouter lists `anthropic/claude-opus-5.5:batch[1m]`, which normalizes
* `-fast` and context hints. Gateway listings match on it. OpenRouter lists
* `anthropic/claude-opus-5.5:batch[1m]`, which normalizes
* to `claude-opus-5-5`; `anthropic/openai/gpt-5` keeps its `openai/` prefix and
* matches no Claude id. */
function normalizeModelId(value: string): string {
@@ -401,24 +407,56 @@ function normalizeModelId(value: string): string {
);
}
/** A path that no model id starts with. Nothing enforces that no
* `deniedModels` entry names it (see {@link deniedModelSpellings}). */
const VERSION_SPELLING_PATH = "version-of/";
/**
* Whether managed `deniedModels` blocks a model id, following the CLI's rules
* closely enough for the picker: a family alias blocks the whole family, and a
* model id blocks that version in every spelling plus the later minor versions
* it prefixes (`claude-opus-5` also blocks `claude-opus-5-5`). Entries share
* `normalizeModelId`, so `claude-opus-4.5` or `anthropic/claude-opus-4.5` also
* blocks `claude-opus-4-5`, which the CLI may still allow. The CLI stays the
* enforcer; this only keeps the picker from offering a refused row.
* The spellings of `id` to ask the SDK's `matchDeniedModels` about. That
* function compares an id with the entries alone. The CLI blocks more
* (`setModel` against CLI 2.1.323):
* - a last path segment that an exact-name entry names: `my-model` blocks
* `gateway/my-model`;
* - a version behind a `:variant` suffix or written with a dot:
* `claude-opus-5-5` blocks `claude-opus-5-5:free` and
* `anthropic/claude-opus-5.5`.
*
* An exact-name entry does not block a `:variant`: `my-model` leaves
* `my-model:free` allowed. So the version spelling goes behind a path, which
* the SDK ignores for a versioned id and which keeps the spelling from
* equalling an exact name.
*/
export function isDeniedModel(modelId: string, deniedModels: readonly string[]): boolean {
const id = normalizeModelId(modelId);
if (!id) return false;
return deniedModels.some((entry) => {
const denied = normalizeModelId(entry);
if (!denied) return false;
if (MODEL_FAMILY_ALIASES.has(denied)) return id === denied || id.includes(`-${denied}`);
return id === denied || id.startsWith(`${denied}-`);
});
function deniedModelSpellings(id: string): string[] {
const lastSegment = id.slice(id.lastIndexOf("/") + 1);
const version = lastSegment.replace(/:.*$/, "").replace(/(?<=\d)\.(?=\d)/g, "-");
return [id, lastSegment, `${VERSION_SPELLING_PATH}${version}`];
}
/**
* Which of `ids` a managed `deniedModels` list blocks, by the CLI's own entry
* rules: the SDK's `matchDeniedModels`, asked about each of the
* {@link deniedModelSpellings} of an id.
* - A family alias (`opus`) blocks the family.
* - A model id blocks that version in every spelling, and the later minor
* versions when it names none (`claude-opus-5` also blocks `claude-opus-5-5`).
* - Any other entry (`claude-opus-4.5`, `my-model`) blocks only that name,
* alone or as the last path segment (`gateway/my-model`).
* - `default`, `best` and `opusplan` block nothing.
*
* The CLI stays the enforcer; this only keeps the picker from offering a row
* that `setModel` would refuse.
*/
export async function deniedModelIds(
ids: readonly string[],
deniedModels: readonly string[],
): Promise<Set<string>> {
if (ids.length === 0 || deniedModels.length === 0) return new Set();
const asked = ids.flatMap((id) => deniedModelSpellings(id).map((spelling) => ({ id, spelling })));
const { denied } = await matchDeniedModels(
asked.map(({ spelling }) => spelling),
deniedModels,
);
return new Set(asked.filter((_, index) => denied[index]).map(({ id }) => id));
}
/**
@@ -436,15 +474,16 @@ export function isDeniedModel(modelId: string, deniedModels: readonly string[]):
* - `undefined` is handled by the caller (no allowlist applied).
* - The Default option is unaffected by `availableModels` — it always remains
* available, even when the allowlist is `[]`.
* - Managed `deniedModels` wins over the allowlist, so a denied entry is
* dropped even though the CLI would otherwise leave it unmatched.
* - Managed `deniedModels` wins over the allowlist: an entry is dropped when
* the list blocks its name, its `modelOverrides` target, or the model of the
* row it resolves to (see {@link deniedModelIds}).
*/
export function applyAvailableModelsAllowlist(
export async function applyAvailableModelsAllowlist(
sdkModels: ModelInfo[],
allowlist: string[],
settingsModelOverrides?: Record<string, string>,
deniedModels: readonly string[] = [],
): ModelInfo[] {
): Promise<ModelInfo[]> {
// Default is always preserved per the docs. Synthesize one if the SDK
// didn't surface it so downstream code (e.g. `getAvailableModels` picking
// `models[0]` as a fallback) still has something to work with.
@@ -465,22 +504,21 @@ export function applyAvailableModelsAllowlist(
// opaque provider IDs) often won't textually resemble anything in
// `sdkModelsWithoutDefault`. Only the entry's surfaced `value` becomes the
// override target, so it's what `setModel` ends up passing to the API.
for (const entry of allowlist) {
const entries = allowlist.flatMap((entry) => {
const trimmed = entry.trim();
if (!trimmed || seen.has(trimmed)) continue;
const overridden = settingsModelOverrides?.[trimmed];
const effective = overridden ?? trimmed;
if (seen.has(effective)) continue;
if (!trimmed) return [];
const effective = settingsModelOverrides?.[trimmed] ?? trimmed;
const sdkMatch = resolveModelPreference(sdkModelsWithoutDefault, trimmed);
if (
[trimmed, effective, sdkMatch?.resolvedModel].some(
(id) => id != null && isDeniedModel(id, deniedModels),
)
) {
continue;
}
const ids = [trimmed, effective, ...(sdkMatch?.resolvedModel ? [sdkMatch.resolvedModel] : [])];
return [{ trimmed, effective, sdkMatch, ids }];
});
const denied = await deniedModelIds(
entries.flatMap(({ ids }) => ids),
deniedModels,
);
for (const { trimmed, effective, sdkMatch, ids } of entries) {
if (seen.has(trimmed) || seen.has(effective)) continue;
if (ids.some((id) => denied.has(id))) continue;
if (sdkMatch) {
result.push({ ...sdkMatch, value: effective });
} else {
@@ -568,10 +606,10 @@ export function narrowToGatewayModels(
* capability flag (current and future); the identity fields are overridden
* because the fuzzy-matched sibling's resolvedModel/displayName/description
* can describe a different context lane and would poison later resolvedModel
* matching (a model switch that the adapter did not make) and context-window
* inference if they traveled under this id. `allowlistedModelInfo` is the
* allowlisted entry alone, the only one whose identity strings are a
* trustworthy window signal.
* matching (a model switch that the adapter did not make) and the lookup of
* the learned context window (`immediateContextWindow`) if they traveled under
* this id. `allowlistedModelInfo` is the allowlisted entry alone, the only one
* whose `resolvedModel` that lookup may use.
*/
export function withCurrentModelInfo(
allowedModels: ModelInfo[],

68
src/session-status.ts Normal file
View File

@@ -0,0 +1,68 @@
/**
* `session/status` (the unstable "Session status" RFD) of a session that the
* connection does not hold: `saved` or unknown. Each core answers `open`
* itself for the sessions it holds.
*/
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { RequestError } from "@agentclientprotocol/sdk/unstable";
import { isSessionId, projectsRoot } from "./paths.js";
/** What a core advertises as `status` in its session capabilities. */
export const SESSION_STATUS_CAPABILITY = {};
/**
* `saved` when a project directory holds a transcript file of `sessionId`
* that is not empty. Its content is not read, so a transcript without a
* conversation is `saved` too, and its resume fails with `-32002`. Fails with
* `-32002` and `data.sessionId` otherwise:
*
* - an id that is not a UUID, such as a subagent's session id;
* - an id without a transcript: one never used, a session never prompted
* (Claude Code writes the transcript at the first prompt), and a session
* whose transcripts were removed.
*
* The request names no `cwd`, so a transcript of any project counts: a
* resume with another `cwd` can still fail with `-32002`.
*
* It lists the project directories and stats the one file name in each. No
* query starts and no session is loaded. A directory or file that cannot be
* read for another reason than being absent fails the request: it does not
* make the session unknown.
*/
export async function savedSessionStatus(sessionId: string): Promise<{ status: "saved" }> {
if (!isSessionId(sessionId) || !(await hasTranscript(sessionId))) {
throw RequestError.sessionNotFound(sessionId);
}
return { status: "saved" };
}
async function hasTranscript(sessionId: string): Promise<boolean> {
const root = projectsRoot();
let projects: string[];
try {
projects = await fs.readdir(root);
} catch (error) {
// No session was ever stored.
if (isAbsent(error)) return false;
throw error;
}
const found = await Promise.all(
projects.map((project) =>
fs.stat(path.join(root, project, `${sessionId}.jsonl`)).then(
(stats) => stats.isFile() && stats.size > 0,
(error: unknown) => {
// No such transcript, or `project` is a file.
if (isAbsent(error)) return false;
throw error;
},
),
),
);
return found.includes(true);
}
function isAbsent(error: unknown): boolean {
const code = (error as { code?: unknown }).code;
return code === "ENOENT" || code === "ENOTDIR";
}

View File

@@ -30,7 +30,14 @@ export const ZERO_USAGE = Object.freeze({
cache_creation_input_tokens: 0,
});
export const DEFAULT_CONTEXT_WINDOW = 200000;
/**
* The context window reported for a model until Claude Code says what it is
* (see `context-window.ts`): 1M, the window Claude Code itself assumes for a
* model id it does not know (since 2.1.323). Claude Code reports 200K for an
* older model such as `claude-sonnet-4-5`, and for every model under
* `CLAUDE_CODE_DISABLE_1M_CONTEXT=1`.
*/
export const DEFAULT_CONTEXT_WINDOW = 1_000_000;
/** `_meta.quota` for work that spent `usage` in its main agent loop and
* `modelUsage` by model (see `turnQuotaMeta` in `v1/acp-agent.ts`). */
@@ -212,26 +219,6 @@ function commonPrefixLength(a: string, b: string) {
return i;
}
/** Best-effort first guess of a model's context window, used to seed the
* window synchronously (via `immediateContextWindow`) until a `result` message
* arrives with the authoritative `modelUsage` value.
*
* Anthropic 1M-context variants encode "1m" as a distinct token in the SDK
* model ID (e.g., "claude-opus-4-6-1m"), which `\b1m\b` catches without also
* matching things like "10m" or embedded substrings. Semantic aliases like
* `default` carry no such token in the ID, but their `resolvedModel` and the
* SDK's human-facing `displayName`/`description` can (e.g.
* "claude-opus-4-8[1m]", "Opus 4.7 (1M context)"), so callers pass those too.
* This text scan can't catch every model — some resolve to extended-context
* models with no "1m" anywhere (e.g. `sonnet` → claude-sonnet-5, natively
* ~1M). Such a miss falls back to the default window, is refined by the
* background `getContextUsage` (`refreshContextWindowInBackground`), and is
* corrected by `result.modelUsage` (and cached) within one turn. */
export function inferContextWindowFromModel(...texts: Array<string | undefined>): number | null {
if (texts.some((text) => text != null && /\b1m\b/i.test(text))) return 1_000_000;
return null;
}
export function getMatchingModelUsage(
modelUsage: Record<string, ModelUsage>,
currentModel: string,

View File

@@ -7,6 +7,7 @@ import {
resolveSettings,
type Settings,
} from "@anthropic-ai/claude-agent-sdk";
import { unloadedPolicySources } from "./managed-policy.js";
import { claudeConfigDir } from "./paths.js";
/**
@@ -37,6 +38,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 +60,11 @@ export class SettingsManager {
private cwd: string;
private effective: Settings = {};
private managedDeniedModels: string[] = [];
private managedModelSettings: ModelSelectionSettings = {};
private managedApiKeyHelper?: string;
private managedCompactionSettings: CompactionSettings = {};
/** The policy sources last logged as not loaded (see {@link unloadedPolicySources}). */
private unloadedPolicy?: string;
private watchers: fs.FSWatcher[] = [];
private onChange?: () => void;
private logger: { log: (...args: any[]) => void; error: (...args: any[]) => void };
@@ -107,20 +121,52 @@ export class SettingsManager {
private async loadAllSettings(): Promise<void> {
try {
const resolved = await resolveSettings({ cwd: this.cwd });
// Once per change: a reload after each settings file edit would repeat it.
const unloaded = unloadedPolicySources(resolved);
if (unloaded !== this.unloadedPolicy) {
this.unloadedPolicy = unloaded;
if (unloaded) {
this.logger.error(
`Managed settings did not load from ${unloaded}: the session's settings ` +
"and model list do not reflect it. Claude Code reads the policy itself.",
);
}
}
this.effective = filterEscalatingDefaultMode(resolved);
// The CLI reads `deniedModels` from managed settings only.
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 +239,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

@@ -20,7 +20,10 @@
* reached the call is that message too: the sender's outgoing view, at the
* call, and the child's incoming one, in the child's history. A child's
* history follows its launch, so a call to a child from its parent is always
* delivered first. Any other SendMessage call stays a tool call.
* delivered first. Any other SendMessage call stays a tool call. As live, a
* TaskStop or SendMessage call that the client is shown and that names a
* replayed child by its agent id carries the RFD's `session` reference to it
* (`ReplayTranscript.withTarget`).
*
* Right after its announcement, each child gets its `model` option: the model
* of its first response, in its transcript. A later response with another
@@ -50,6 +53,8 @@ import {
rfdLabels,
sameDirectory,
type SubagentModels,
targetAgentId,
withSessionReference,
type WorkState,
} from "./native-subagents.js";
import { noticeUpdate } from "./session-notices.js";
@@ -116,9 +121,29 @@ export class ReplayTranscript<Child extends ReplayChild = ReplayChild> {
readonly omitted = new Set<string>();
/** The SubagentHandback calls, by tool use id. */
readonly handbacks = new Map<string, ReplayHandback>();
/** The agent that each TaskStop or SendMessage call names (see `targetAgentId`), by tool use id. */
private readonly targets = new Map<string, string>();
constructor(private readonly history: SubagentHistory) {}
/**
* `notification`, a replayed tool call update that the client is shown on
* the session `shownOn`, with the RFD's `session` reference when the call
* names a child of another session by its agent id, as live
* (`NativeSubagentRuntime.withTarget`). Only a child that the replay
* announced before the update is referenced.
*/
withTarget(notification: AcpSessionNotification, shownOn: string): AcpSessionNotification {
const { update } = notification;
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
return notification;
}
const agentId = this.targets.get(update.toolCallId);
const child = agentId !== undefined ? this.byAgentId.get(agentId) : undefined;
if (!child?.announced || child.sessionId === shownOn) return notification;
return withSessionReference(notification, child.sessionId);
}
/**
* Registers the launches, the launch results, and the SendMessage calls in
* `messages`, the transcript of the session `ownerSessionId`.
@@ -127,6 +152,8 @@ export class ReplayTranscript<Child extends ReplayChild = ReplayChild> {
for (const message of messages) {
for (const block of contentBlocks(message)) {
if (block.type === "tool_use" && typeof block.id === "string") {
const agentId = targetAgentId(block.name, block.input);
if (agentId !== undefined) this.targets.set(block.id, agentId);
if (isNativeSubagentControlTool(block.name)) {
this.registerLaunch(block.id, inputOf(block), ownerSessionId);
} else if (block.name === HANDBACK_TOOL) {
@@ -214,6 +241,11 @@ export class RfdSubagentReplay {
}
/** The child that the tool use `toolUseId` launched, when the replay shows it. */
/** See {@link ReplayTranscript.withTarget}. */
withTarget(notification: AcpSessionNotification, shownOn: string): AcpSessionNotification {
return this.transcript.withTarget(notification, shownOn);
}
childOfLaunch(toolUseId: string): ReplayChild | undefined {
return this.transcript.children.get(toolUseId);
}

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":{},"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":{},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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":{}},"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

@@ -28,6 +28,16 @@
* - The `initialize` response also advertises `agentCapabilities.auth.status`,
* which origin/main did not: the agent now answers `auth/status` and sends
* `auth/status_update`.
* - 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
@@ -229,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;
}
@@ -264,14 +272,18 @@ 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);
});
const current = withoutSandboxNetworkSettlements(
traffic.map((record) =>
withoutAuthStatusCapability(withoutSessionCapability(record, "systemPrompt")),
withoutAuthStatusCapability(
withoutSessionCapabilities(record, ["systemPrompt", "status", "title", "options"]),
),
),
);
const violations: string[] = [];

View File

@@ -16,6 +16,22 @@ import {
const bashInput = { command: "ls -la", description: "List files" };
/**
* The heartbeat of a PreToolUse or PostToolUse hook that runs long, as Claude
* Code sends it every 30 seconds since 2.1.323: `tool_name` is the hook
* event, and `parent_tool_use_id` is the tool call the hook runs for.
*/
const hookHeartbeat = (event: "PreToolUse" | "PostToolUse", toolUseId: string, uuid: string) => ({
type: "tool_progress",
tool_use_id: `hook-${event}-${toolUseId}-heartbeat-0`,
tool_name: event,
parent_tool_use_id: toolUseId,
elapsed_time_seconds: 30,
heartbeat: true,
uuid,
session_id: activeSdkSessionId(),
});
export const SCENARIOS: Scenario[] = [
{
name: "session-setup",
@@ -1118,6 +1134,9 @@ export const SCENARIOS: Scenario[] = [
yield* assistantTurn("msg_progress", [
{ type: "tool_use", id: "toolu_slow", name: "Bash", input: { command: "make" } },
]);
// The heartbeats of its hooks report no progress of the tool: the
// client gets nothing for them.
yield hookHeartbeat("PreToolUse", "toolu_slow", "00000000-0000-4000-8000-00000000b004");
for (const seconds of [1, 2, 3]) {
yield {
type: "tool_progress",
@@ -1129,6 +1148,7 @@ export const SCENARIOS: Scenario[] = [
session_id: activeSdkSessionId(),
};
}
yield hookHeartbeat("PostToolUse", "toolu_slow", "00000000-0000-4000-8000-00000000b005");
yield toolResult("toolu_slow", "built", {
structured: { stdout: "built", stderr: "", interrupted: false, isImage: false },
});

View File

@@ -9,10 +9,13 @@ 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
* don't shift in tests that don't care about context usage. */
/** The context-usage report the base mock query returns: a 200K window, as
* Claude Code reports for `claude-sonnet-4-5`. It differs from the agent's
* DEFAULT_CONTEXT_WINDOW, so a test can tell a window that was read from the
* default. */
export const DEFAULT_CONTEXT_USAGE = { totalTokens: 0, rawMaxTokens: 200000 };
/**
@@ -82,3 +85,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

@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import type { resolveSettings } from "@anthropic-ai/claude-agent-sdk";
import { applyManagedPolicyEnv } from "../managed-policy.js";
import { applyManagedPolicyEnv, unloadedPolicySources } from "../managed-policy.js";
type ResolveSettings = typeof resolveSettings;
@@ -57,6 +57,25 @@ describe("applyManagedPolicyEnv", () => {
);
});
it("reports a policy source that did not load on stderr, and still applies the rest", async () => {
const env: Record<string, string | undefined> = {};
const stderr = vi.spyOn(console, "error").mockImplementation(() => {});
await applyManagedPolicyEnv(
resolverReturning({
effective: { env: { FROM_FILE: "1" } },
machinePolicy: { didNotLoad: ["profile"] },
}),
env,
);
expect(env).toEqual({ FROM_FILE: "1" });
expect(stderr).toHaveBeenCalledWith(
"Managed settings did not load from the macOS MDM profile: its env vars are not applied. " +
"Restart the agent to read it again.",
);
});
it("asks the SDK for the managed tier only", async () => {
const seen: unknown[] = [];
const resolve = (async (options: unknown) => {
@@ -69,3 +88,18 @@ describe("applyManagedPolicyEnv", () => {
expect(seen).toEqual([{ settingSources: [] }]);
});
});
describe("unloadedPolicySources", () => {
it.each([
// What the SDK reports when every source loaded.
[{ machinePolicy: { didNotLoad: false as const } }, undefined],
// An SDK before 0.3.323 leaves the member out.
[{}, undefined],
[
{ machinePolicy: { didNotLoad: ["registry" as const, "file" as const] } },
"the Windows registry policy, the managed-settings file",
],
])("names the sources of %j", (resolved, expected) => {
expect(unloadedPolicySources(resolved)).toBe(expected);
});
});

View File

@@ -168,7 +168,7 @@ describe("versioned model display names", () => {
{ value: "haiku", displayName: "Haiku", description: "Fast" },
];
const ids = ["claude-opus-5-5[1m]", "claude-opus-5-5", "opus[1m]", "opus", "haiku"];
const models = applyAvailableModelsAllowlist(sdkModels, ids);
const models = await applyAvailableModelsAllowlist(sdkModels, ids);
const setModel = vi.fn();
const state = await getAvailableModels(
{ setModel } as unknown as Query,

View File

@@ -7,7 +7,7 @@ import {
aliasResumedModelRow,
resolveModelPreference,
applyAvailableModelsAllowlist,
isDeniedModel,
deniedModelIds,
matchResumedModel,
modelEntryFor,
} from "../session-model.js";
@@ -222,8 +222,8 @@ describe("matchResumedModel", () => {
it("prefers an exact named-row resolvedModel match over the hint-stripped Default tier", () => {
// "claude-sonnet-5[1m]" differs from Default's "claude-sonnet-5" only by
// the context hint; stripping both would misreport the explicit 1M row as
// Default (and infer the wrong context window downstream). Both hint
// spellings must land on the row regardless of which one it uses.
// Default. Both hint spellings must land on the row regardless of which
// one it uses.
expect(matchResumedModel(SONNET_DEFAULT_MODELS, "claude-sonnet-5[1m]").value).toBe(
"sonnet[1m]",
);
@@ -304,8 +304,8 @@ describe("aliasResumedModelRow", () => {
});
describe("applyAvailableModelsAllowlist - resolvedModel matching", () => {
it("resolves an allowlist entry pinned to a canonical resolvedModel id", () => {
const result = applyAvailableModelsAllowlist(LIVE_SHAPED_MODELS, ["claude-sonnet-5"]);
it("resolves an allowlist entry pinned to a canonical resolvedModel id", async () => {
const result = await applyAvailableModelsAllowlist(LIVE_SHAPED_MODELS, ["claude-sonnet-5"]);
const entry = result.find((m) => m.value === "claude-sonnet-5");
expect(entry).toBeDefined();
expect(entry?.displayName).toBe("Sonnet");
@@ -324,14 +324,14 @@ describe("applyAvailableModelsAllowlist - modelOverrides", () => {
},
];
it("surfaces the override target as the value while keeping the alias's display metadata", () => {
it("surfaces the override target as the value while keeping the alias's display metadata", async () => {
// The override target is an opaque Bedrock ARN with no "opus" substring,
// so matching against it (instead of the "claude-opus-4-6" alias) would
// find nothing and silently drop displayName/description/supportsEffort.
const overrides = {
"claude-opus-4-6": "arn:aws:bedrock:us-west-2:111122223333:inference-profile/custom-7f3a",
};
const result = applyAvailableModelsAllowlist(SDK_MODELS, ["claude-opus-4-6"], overrides);
const result = await applyAvailableModelsAllowlist(SDK_MODELS, ["claude-opus-4-6"], overrides);
const entry = result.find((m) => m.value === overrides["claude-opus-4-6"]);
expect(entry).toEqual({
@@ -345,8 +345,8 @@ describe("applyAvailableModelsAllowlist - modelOverrides", () => {
});
describe("applyAvailableModelsAllowlist - managed deniedModels", () => {
it("drops an allowlisted model that deniedModels blocks, keeping Default", () => {
const result = applyAvailableModelsAllowlist(
it("drops an allowlisted model that deniedModels blocks, keeping Default", async () => {
const result = await applyAvailableModelsAllowlist(
LIVE_SHAPED_MODELS,
["claude-sonnet-5", "claude-opus-5-5"],
undefined,
@@ -355,43 +355,87 @@ describe("applyAvailableModelsAllowlist - managed deniedModels", () => {
expect(result.map((m) => m.value)).toEqual(["default", "claude-sonnet-5"]);
});
it("checks an alias entry against the row it resolves to", () => {
const result = applyAvailableModelsAllowlist(LIVE_SHAPED_MODELS, ["sonnet"], undefined, [
it("checks an alias entry against the row it resolves to", async () => {
const result = await applyAvailableModelsAllowlist(LIVE_SHAPED_MODELS, ["sonnet"], undefined, [
"claude-sonnet-5",
]);
expect(result.map((m) => m.value)).toEqual(["default"]);
});
it("checks an entry against its modelOverrides target", async () => {
const result = await applyAvailableModelsAllowlist(
LIVE_SHAPED_MODELS,
["sonnet", "haiku"],
{
haiku:
"arn:aws:bedrock:us-east-1:123456789012:inference-profile/us.anthropic.claude-opus-5-5-v1:0",
},
["claude-opus-5-5"],
);
expect(result.map((m) => m.value)).toEqual(["default", "sonnet"]);
});
});
describe("isDeniedModel", () => {
it("blocks every spelling of a denied version and its later minors", () => {
const denied = ["claude-opus-5"];
expect(isDeniedModel("claude-opus-5", denied)).toBe(true);
expect(isDeniedModel("claude-opus-5-20260101", denied)).toBe(true);
expect(isDeniedModel("claude-opus-5[1m]", denied)).toBe(true);
expect(isDeniedModel("us.anthropic.claude-opus-5-v1:0", denied)).toBe(true);
expect(isDeniedModel("claude-opus-5-5", denied)).toBe(true);
expect(isDeniedModel("claude-opus-4-8", denied)).toBe(false);
expect(isDeniedModel("anthropic/claude-opus-5.5:batch[1m]", denied)).toBe(true);
expect(isDeniedModel("anthropic/us.anthropic.claude-opus-5-v1:0", denied)).toBe(true);
});
it("reads an entry in OpenRouter or dotted spelling as the dashed SDK id", () => {
expect(isDeniedModel("claude-opus-4-5", ["claude-opus-4.5"])).toBe(true);
expect(isDeniedModel("claude-opus-5-5", ["anthropic/claude-opus-5.5"])).toBe(true);
expect(isDeniedModel("claude-opus-5", ["anthropic/claude-opus-5.5"])).toBe(false);
});
it("blocks only that version when the entry names a minor", () => {
expect(isDeniedModel("claude-opus-5", ["claude-opus-5-5"])).toBe(false);
expect(isDeniedModel("claude-opus-5-5-fast", ["claude-opus-5-5"])).toBe(true);
});
it("blocks a whole family for a family alias and ignores settings-dependent aliases", () => {
expect(isDeniedModel("claude-haiku-4-5-20251001", ["haiku"])).toBe(true);
expect(isDeniedModel("haiku", ["haiku"])).toBe(true);
expect(isDeniedModel("claude-sonnet-5", ["haiku"])).toBe(false);
expect(isDeniedModel("claude-opus-5", ["default", "best", "opusplan"])).toBe(false);
describe("deniedModelIds", () => {
// What CLI 2.1.323 answered `setModel(id)` under each managed `deniedModels`
// list: it refused the `blocked` ids as restricted by the organization, and
// not the `allowed` ones.
it.each<{ name: string; denied: string[]; blocked: string[]; allowed: string[] }>([
{
name: "a family alias blocks the family",
denied: ["opus"],
blocked: ["opus[1m]", "vertex_ai/claude-opus-5-5", "claude-opus-5.5", "my-opus-proxy"],
allowed: ["opusplan", "anthropic/openai/gpt-5", "claude-sonnet-5.5"],
},
{
name: "a version without a minor blocks its later minors",
denied: ["claude-opus-5"],
blocked: ["claude-opus-5", "claude-opus-5-5", "us.anthropic.claude-opus-5-5-v1:0"],
allowed: ["opus", "claude-opus-4-5"],
},
{
name: "a version with a minor blocks that version in every spelling",
denied: ["claude-opus-5-5"],
blocked: [
"claude-opus-5-5-fast",
"claude-opus-5-5[1m]",
"claude-opus-5-5@20260101",
"openrouter/anthropic/claude-opus-5-5",
],
allowed: ["claude-opus-5", "opus", "claude-opus-5-50"],
},
{
name: "a version is blocked behind a :variant suffix and written with a dot",
denied: ["claude-opus-5-5"],
blocked: [
"claude-opus-5-5:free",
"claude-opus-5-5:batch[1m]",
"anthropic/claude-opus-5.5",
"x/claude-opus-5.5:free",
"claude-opus-5.5-20260101",
],
allowed: ["claude-opus-5.50", "anthropic/claude-opus-5"],
},
{
name: "an entry that is no version blocks that exact name, also as the last path segment",
denied: ["my-model"],
blocked: ["My-Model", "gateway/my-model", "a/b/my-model", "gateway/my-model[1m]"],
allowed: ["my-model:free", "gateway/my-model:free", "my-model-2", "my-model/x"],
},
{
name: "a dotted entry is an exact name, not a version",
denied: ["claude-opus-4.5"],
blocked: ["claude-opus-4.5", "anthropic/claude-opus-4.5"],
allowed: ["claude-opus-4-5", "claude-opus-4-5-20251101", "claude-opus-4.5:batch"],
},
{
name: "an alias whose model depends on the release blocks nothing",
denied: ["default", "best", "opusplan"],
blocked: [],
allowed: ["default", "best", "opusplan", "opus"],
},
])("$name", async ({ denied, blocked, allowed }) => {
expect([...(await deniedModelIds([...allowed, ...blocked], denied))]).toEqual(blocked);
});
});

View File

@@ -0,0 +1,120 @@
import { describe, it, expect, afterEach, vi } from "vitest";
import * as os from "node:os";
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" },
});
});
});
describe("a managed policy source that did not load", () => {
let manager: SettingsManager;
afterEach(() => manager?.dispose());
it("is logged once while the same source stays unloaded", async () => {
vi.mocked(resolveSettings).mockResolvedValue({
effective: {},
provenance: {},
sources: [],
machinePolicy: { didNotLoad: ["profile"] },
});
const error = vi.fn();
manager = new SettingsManager(process.cwd(), { logger: { log: () => {}, error } });
await manager.initialize();
// Another cwd resolves the settings again.
await manager.setCwd(os.tmpdir());
expect(resolveSettings).toHaveBeenCalledTimes(2);
expect(error.mock.calls).toEqual([
[
"Managed settings did not load from the macOS MDM profile: the session's settings " +
"and model list do not reflect it. Claude Code reads the policy itself.",
],
]);
});
});

View File

@@ -2012,6 +2012,164 @@ describe("NativeSubagentRuntime in the RFD form", () => {
expect(states(published).at(-1)).toEqual({ state: "unknown" });
});
it("reports a child that may go on working unobserved as unknown, without its cancel, and keeps an idle one", async () => {
const { runtime, published } = await runningWorker();
await runtime.route(launch("launch-2", { description: "Read the docs" }), async () => {});
await runtime.taskStarted(
{ taskId: "worker-2", toolUseId: "launch-2", subagentType: "Explore" },
async () => {},
);
await runtime.finishTask("worker-2", "completed", async () => {}, "launch-2");
published.length = 0;
// A conversation reset: Claude Code can keep the children running.
await runtime.finishAll("failed", async () => {}, { mayStillRun: true });
expect(published.map(({ update }) => update)).toEqual([
{
sessionUpdate: "subagent_update",
sessionId: "worker-1",
capabilities: null,
state: { state: "unknown" },
},
]);
});
describe("a tool call that operates on a child", () => {
const toolCall = (
toolCallId: string,
toolName: string,
rawInput: Record<string, unknown>,
parentToolUseId?: string,
): AcpSessionNotification =>
({
sessionId: "root",
update: {
sessionUpdate: "tool_call",
toolCallId,
title: toolName,
status: "pending",
rawInput,
content: [{ type: "content", content: { type: "text", text: "input" } }],
_meta: {
claudeCode: { toolName, ...(parentToolUseId ? { parentToolUseId } : {}) },
},
},
}) as AcpSessionNotification;
const toolResult = (
toolCallId: string,
toolName: string,
content?: unknown[],
): AcpSessionNotification =>
({
sessionId: "root",
update: {
sessionUpdate: "tool_call_update",
toolCallId,
status: "completed",
...(content ? { content } : {}),
_meta: { claudeCode: { toolName } },
},
}) as AcpSessionNotification;
const text = { type: "content", content: { type: "text", text: "Stopped" } };
const reference = { type: "session", sessionId: "worker-1" };
const contentOf = (notification: AcpSessionNotification | null) =>
(notification?.update as { content?: unknown[] } | undefined)?.content;
it("references the announced child that a TaskStop call names by its agent id", async () => {
const { runtime } = await runningWorker();
expect(
contentOf(
await runtime.route(
toolCall("stop-1", "TaskStop", { task_id: "worker-1" }),
async () => {},
),
),
).toEqual([{ type: "content", content: { type: "text", text: "input" } }, reference]);
// An update without content would replace nothing, so it carries none.
const progress = toolResult("stop-1", "TaskStop");
(progress.update as { status?: string }).status = "in_progress";
expect(contentOf(await runtime.route(progress, async () => {}))).toBeUndefined();
// The result's content replaces the first report's, so it has the reference again.
expect(
contentOf(await runtime.route(toolResult("stop-1", "TaskStop", [text]), async () => {})),
).toEqual([text, reference]);
});
it.each([
["a task that is no child", "TaskStop", { task_id: "bash_1" }],
["a child named by its name, which Claude Code resolves", "TaskStop", { task_id: "builder" }],
["another tool with the same input", "Monitor", { task_id: "worker-1" }],
])("references nothing for %s", async (_, toolName, input) => {
const { runtime } = await runningWorker({ name: "builder" });
expect(
contentOf(await runtime.route(toolCall("call-1", toolName, input), async () => {})),
).toEqual([{ type: "content", content: { type: "text", text: "input" } }]);
});
it("references no child that is not announced yet, as the RFD requires", async () => {
const { runtime } = await runningWorker();
// A nested child whose launch frame, which names its parent, has not come.
await runtime.taskStarted(
{ taskId: "worker-2", toolUseId: "launch-2", subagentType: "Explore" },
async () => {},
);
await runtime.route(launch("launch-2", { description: "Dig" }, "launch-9"), async () => {});
expect(
contentOf(
await runtime.route(
toolCall("stop-2", "TaskStop", { task_id: "worker-2" }),
async () => {},
),
),
).toEqual([{ type: "content", content: { type: "text", text: "input" } }]);
});
it("references the child from a SendMessage call to it that stays a tool call", async () => {
const { runtime } = await runningWorker();
// The message resumes no child, so the call is shown at its end.
await expect(
runtime.route(toolCall("send-1", "SendMessage", { to: "worker-1" }), async () => {}),
).resolves.toBeNull();
const shown = await runtime.route(
toolResult("send-1", "SendMessage", [text]),
async () => {},
);
expect(shown).toMatchObject({ sessionId: "root", update: { sessionUpdate: "tool_call" } });
expect(contentOf(shown)).toEqual([text, reference]);
});
it("references the child from its launch call that a permission request showed", async () => {
const { runtime } = await runningWorker();
const shown = await runtime.route(
launchResult("launch-1", "completed", "Done"),
async () => {},
"root",
);
expect(contentOf(shown)).toEqual([
{ type: "content", content: { type: "text", text: "Done" } },
reference,
]);
});
it("references nothing in AIR's draft", async () => {
const runtime = new NativeSubagentRuntime(
"air",
"root",
sessionWith(child({ sessionId: "worker-1", announced: true })),
async () => {},
{ log: () => {} },
);
const call = toolCall("stop-1", "TaskStop", { task_id: "worker-1" });
await expect(runtime.route(call, async () => {})).resolves.toEqual(call);
});
});
it("reports a child as requires_action while its requests are open", async () => {
const { runtime, published } = await runningWorker();
const first = Promise.withResolvers<string>();

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,63 @@
import { randomUUID } from "node:crypto";
import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
import { describe, expect, it } from "vitest";
import { RetractedToolCalls } from "../tool-calls/retracted.js";
/** An assistant message that calls the tools `toolUseIds`, as the SDK sends it. */
function assistant(toolUseIds: string[], supersedes?: string[]) {
return {
type: "assistant",
message: {
id: "msg_1",
type: "message",
role: "assistant",
model: "claude-fable-5",
content: toolUseIds.map((id) => ({ type: "tool_use", id, name: "Read", input: {} })),
stop_reason: null,
stop_sequence: null,
usage: { input_tokens: 1, output_tokens: 1 },
},
parent_tool_use_id: null,
uuid: randomUUID(),
session_id: "session",
...(supersedes && { supersedes }),
} as unknown as SDKMessage & { uuid: string };
}
/** The notice of a refusal fallback that took back the messages `retracted`. */
const notice = (retracted: string[]) =>
({
type: "system",
subtype: "model_refusal_fallback",
retracted_message_uuids: retracted,
}) as unknown as SDKMessage;
describe("RetractedToolCalls", () => {
it("gives the tool calls of a message once, though the notice and the retry both name it", () => {
const calls = new RetractedToolCalls();
const refused = assistant(["toolu_a", "toolu_b"]);
calls.sent(refused);
expect(calls.retractedBy(notice([refused.uuid]))).toEqual(["toolu_a", "toolu_b"]);
expect(calls.retractedBy(assistant([], [refused.uuid]))).toEqual([]);
});
it("leaves out a tool call that the replacing message makes itself", () => {
const calls = new RetractedToolCalls();
const refused = assistant(["toolu_gone", "toolu_again"]);
calls.sent(refused);
expect(calls.retractedBy(assistant(["toolu_again"], [refused.uuid]))).toEqual(["toolu_gone"]);
});
it("forgets the oldest of more than 1000 messages", () => {
const calls = new RetractedToolCalls();
const oldest = assistant(["toolu_oldest"]);
calls.sent(oldest);
const others = Array.from({ length: 1000 }, (_, i) => assistant([`toolu_${i}`]));
for (const message of others) calls.sent(message);
expect(calls.retractedBy(notice([oldest.uuid]))).toEqual([]);
expect(calls.retractedBy(notice([others[0]!.uuid]))).toEqual(["toolu_0"]);
});
});

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 { 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,
isSessionId,
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:

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