Compare commits

..

19 Commits

Author SHA1 Message Date
mark.tkachenko
a39e511c97 refactor: move AcpClient and Logger out of acp-agent.ts 2026-11-09 13:02:48 +01:00
Ben Brandt
a58293f4c9 fix: update to @anthropic-ai/claude-agent-sdk 0.3.319 (#1545)
Updates the Claude Agent SDK from 0.3.318 to 0.3.319, which bundles
Claude Code 2.1.319. Also updates `@anthropic-ai/sdk` (1.4.0 → 1.6.0)
and the transitive dependencies in the lockfile (`@babel/runtime`,
`@eslint-community/regexpp`, `@rolldown/pluginutils`, `hono`, `jose`,
`vite`).

## Reviewed, no change needed

- **`mcp_task` background tasks:** SDK sessions get these only with
`CLAUDE_AUTO_BACKGROUND_TASKS=1` and streaming input. The adapter does
not set that variable, so they don't occur by default. If a user sets
it, the Async Tasks panel shows the raw `mcp_task` type, and the
`resource_links` on the task notification are not forwarded. Left as a
possible follow-up.
- **`deleteSession()` rejects with `ENOTEMPTY`** when the session folder
is only partly removed. `session/delete` already rethrows any error
other than "not found".
- **`session_writes_lost`** is now sent only when the CLI exits 0. The
adapter only logs it.
- **`permissions.denyInsteadOfPrompting`:** with it set, the CLI denies
instead of calling `canUseTool`. That is the setting's intent.
- **`mcp_call.check_permissions`, `set_has_subscribers`,
`disableWindowsBashCommandSplit`:** the adapter does not use these.
- The comments on the `getSettings` and `unanswered` type workarounds
now name 0.3.319; both are still missing from the SDK's types.

## Not updated

TypeScript stays on 6.0.3: typescript-eslint 8.72.1 requires `typescript
<6.1.0`, so 7.0.2 has to wait for a typescript-eslint release that
supports it.

## Testing

`npm run build`, `npm run check` and `npm run test:run` pass. `npm run
test:integration` was not run.
2026-11-09 08:56:37 +00:00
PerryLink
3b22586cf6 fix(test): assert the blockquoted MCP startup transcript line (#1543)
`#1362` moved every transcript fallback for clients without
`session.notices` into its own blockquote paragraph (`\n\n> …\n\n`) and
updated the existing assertions, but missed the MCP server startup one.
`main` has been red since that merge:

- The `Build` job on `47b64c0` fails with an annotation at
`src/tests/acp-agent.test.ts` line 13860:

  ```
AssertionError: expected '\n\n> **MCP server "client-fs" failed…' to be
'**MCP server "client-fs" failed to st…'
  ```

- The neighbouring fallback tests already assert the blockquote form,
e.g. `"\n\n> **Warning:** hook says no\n\n"` and the two-line `Stop
says:` message.

This updates the expected string to the blockquoted text; product code
is untouched.

## Validation

- `npx vitest run src/tests/acp-agent.test.ts -t "falls back to a
transcript line without the notices capability"` → 1 passed
- `npx vitest run src/tests/acp-agent.test.ts` → 759 passed | 9 skipped
- `npm run check` → passes
2026-11-08 07:57:15 +00:00
alexwan
47b64c04de fix: start transcript notices on their own line for clients without session.notices (#1362)
## What

When a client does not advertise `session.notices`, a notice falls back
to an `agent_message_chunk` like `**Notice:** …`. Clients render
consecutive chunks as one markdown stream, so the fallback is glued to
Claude's last sentence. In Zed 1.24 a Stop hook's `systemMessage` shows
up as:

```
…推上去。**Notice:** Stop says: token: 3 calls
Stop says: hand off now
```

The fallback is now its own blockquote paragraph, with blank lines on
both sides:

```
…推上去。

> **Notice:** Stop says: token: 3 calls
> hand off now
```

## Behavior

- `noticeOrTranscriptUpdate` wraps the transcript text with
`transcriptBlock`: a leading blank line so it starts on a new line, `> `
on every line, and a trailing blank line so Claude's next chunk does not
continue the quote. This covers every transcript fallback (hook notices,
"Task stopped by user", "Fast mode turned off", "Auto mode
unavailable").
- Claude Code prefixes every line of a hook's `systemMessage` with
`<HookEvent> says: `. The informational fallback keeps that prefix on
the first line only, and only when every line repeats the same prefix
(`collapseHookSpeaker`).
- Clients that advertise `session.notices` get the same `notice` update
as before.
- `Turn.noticeTexts` still records `message.content` unchanged, so the
hook-blocked result de-duplication keeps working.

## Tests

- New unit tests for `transcriptBlock` and `collapseHookSpeaker`, plus
an end-to-end test for a two-line `notice`-level hook message.
- Updated the existing transcript fallback assertions to the blockquote
form.
- `npm run check`, `npm run build` and `npm run test:run` pass (3111
tests).

Co-authored-by: alex <hongchang.wan@longbridge-inc.com>
2026-11-06 22:14:44 +00:00
Jonathan Ellis
1ee2422934 fix: keep autonomous turn tasks open across owed trailing idles (#1455)
When Claude Code starts a new autonomous cycle before the previous
cycle's trailing idle arrives, that late idle completes the new cycle's
AIR async task while Claude is still working. The next output then
creates a second task for the same cycle.

This change completes the autonomous task inside the existing cycle-end
branch, which already checks that `owedTrailingIdles` is zero. An owed
idle is now only consumed by the existing accounting and leaves the
current task open. The task still completes at its own terminal result
or at an unowed idle, so the task and the cycle end on the same
decision.

It adds a scripted SDK-stream regression for two consecutive autonomous
cycles with no running transition between them. The test checks that the
late idle leaves the second task open and that later output stays on the
same task, for a cycle that ends at a result and for one that ends at an
unowed idle. Both cases fail before the fix and pass after it.

Fixes #1453.

Validation, rebased on 0.98.0 (c90f856):
- `npm run build` and `npm run check` pass.
- `npm run test:run`: 3,378 passed, 32 skipped. One unrelated test,
`tools.test.ts` "picks up a rewrite that restored the modification time
of the file", failed once in the full run on a heavily loaded machine.
It passed 3 of 3 runs alone, both on this branch and on `main`, and this
change does not touch `tools.ts` or its tests.

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 22:11:36 +00:00
Kerekes Dániel
7d1ef76c1d fix: report setup-token logins as logged in (#1376)
Fixes #1295

With a `claude setup-token` login (`CLAUDE_CODE_OAUTH_TOKEN`), `claude
auth status --json` only returns `{"loggedIn": true, "authMethod":
"oauth_token", "apiProvider": "firstParty"}`. `fromCliStatus()` handed
that to `mapAuthFields()`, which has no plan or key fields to go on and
fell back to "Not logged in". Clients then showed a working session as
logged out.

This maps that exact state to `kind: "account"` with the label suggested
in the issue ("Claude subscription", detail "OAuth token"). Everything
else still goes through `mapAuthFields()`.

Added a case to `auth-status.test.ts` that fails without the change.
`npm run check`, `npm run build` and `npm run test:run` pass.
2026-11-06 23:10:04 +01:00
Hussain Hammad
aa0c1234b5 fix: replay a compaction summary by its flag, not its framing (#1536)
Fixes #1535

A compaction summary on replay was only sent as a `compaction_update` if
its text also parsed, so a change in Claude Code's framing turned it
into a `user_message_chunk`, shown as text the user typed. That is what
happens to summaries from Claude Code 2.1.312 and 2.1.316.

`isCompactSummary` now decides on its own. A summary whose framing isn't
recognized is sent as a completed `compaction_update` without summary
text. This reverses the choice to keep such text visible: the
model-facing framing isn't kept as a summary either way, and showing it
as the user's words is the worse failure.

`PERSISTED_PREAMBLE` also allows content before the `Summary:` line, so
current summaries keep their text. All 31 summaries in my own
transcripts now parse, and a real `session/load` on one from 2.1.316
sends a `compaction_update` with its text and no summary
`user_message_chunk`.
2026-11-06 22:09:30 +00:00
Alexis Rouillard
54392e144d fix: relay a rate_limit_event that arrives before the turn's first assistant frame (#1534)
## Problem

`rate_limit_event` is relayed to the client as
`usage_update._meta["_claude/rateLimit"]`, but only when
`lastAssistantTotalUsage !== null`. With partial messages on, the SDK
emits `rate_limit_event` before the turn's first `assistant` message,
while `lastAssistantTotalUsage` is still null (reset at each turn). The
SDK emits it once per turn, so in practice clients never receive
`_claude/rateLimit`.

Observed with a real subscription session (SDK 0.3.316): the SDK sends
`stream_event`s, then `rate_limit_event`, then the first `assistant`.
The adapter dropped the event and sent only `usage_update`s with
`_claude/model`.

## Fix

When the event arrives before the turn has any usage, keep its
`rate_limit_info` and attach it to the next `usage_update`. That happens
in `attachUsageModel`, which every `usage_update` already goes through.
A rate limit that the update already carries wins over the pending one.
When usage is already known, the event is sent right away, as before.

## Tests

- New test in `acp-agent.test.ts`: a `rate_limit_event` before the first
assistant frame ends up on the turn's first `usage_update`, and on no
later one. It fails without the fix.
- `npm run check`, `npm run build` and `npm run test:run` pass.
- Checked by hand over stdio with a real session: the turn's first
`usage_update` now carries `_claude/rateLimit`, including
`unifiedWindows.seven_day`.
2026-11-06 22:07:33 +00:00
Ben Brandt
6597626d86 fix: list a subagent's background Bash commands (#1540)
Claude Code sends no `tool_use_result` with a subagent's tool results.
The adapter recognizes a background Bash command only from the
`backgroundTaskId` in that structured result, so it never recognized one
that a subagent started. The command's terminal was reported exited
(code 0) as soon as the result came, while the command kept running, and
the command was missing from the subagent's `state_update.background`.

The real SDK stream (CLI 2.1.318) for a subagent running `sleep 30` in
the background:

```
task_started  task_id=beure6fer task_type=local_bash tool_use_id=toolu_018M… owned_by_subagent=true is_backgrounded=true
user          parent_tool_use_id=toolu_01HY… tool_result for toolu_018M…, no tool_use_result
```

The local transcripts agree: none of the 35 subagent background Bash
results across CLI 2.1.293 to 2.1.318 has a `toolUseResult`.

When a Bash result has no structured task id, the adapter now uses the
live background `local_bash` task whose `task_started` named that Bash
call. The existing subagent test now sends the stream the CLI sends. It
fails without the fix.
2026-11-06 19:12:42 +01:00
Ben Brandt
ba162346c9 fix: list Monitor calls in the session's background work (#1538)
Claude Code marks every live-update watcher `ambient`, "requested or
auto-started" per the SDK's `background_tasks_changed` docs, so the task
of a `Monitor` call the model made is ambient too. `background-work.ts`
left out every ambient task, so a running Monitor was never listed:

- its tool call went straight to `completed` instead of staying
`in_progress` until its task ends;
- a client couldn't stop it with `tool_call/cancel`;
- a client deciding whether a session is quiet, for example before
restarting or releasing it, saw no background work while the Monitor
kept starting turns.

Seen live in Delta against 0.101.0: a Monitor watching a loop kept
waking the agent every 20 seconds while every `state_update` reported
`background: []`.

## Change

- `toolResults` registers a task a tool call started even when it is
ambient, so the call stays `in_progress` and advertises `cancel`.
- The list builder lists a registered call's task before skipping
ambient tasks. Ambient tasks without a call of their own, such as
watchers Claude Code starts by itself, and ambient terminals stay out.

## Tests

- The Monitor fixture in `acp-v2.test.ts` now carries `ambient: true`,
as the CLI sends it. Without the fix, the four existing Monitor tests
fail with it.
- The `_claude/task` test is now an `it.each`: a non-ambient task
without a call is listed, and an ambient watcher without a call is not.
- `npm run check`, `npm run build`, `npm run test:run` pass. Not run:
`npm run test:integration` against the real CLI.
2026-11-06 12:12:58 +01:00
acp-release-bot[bot]
85aec26ef9 chore(main): release 0.101.0 (#1516)
🤖 I have created a release *beep* *boop*
---


##
[0.101.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.100.0...v0.101.0)
(2026-11-05)


### Features

* compact idle sessions before the prompt cache expires
([#1522](https://github.com/agentclientprotocol/claude-agent-acp/issues/1522))
([a79d073](a79d07324d)),
closes
[#1454](https://github.com/agentclientprotocol/claude-agent-acp/issues/1454)
[#1518](https://github.com/agentclientprotocol/claude-agent-acp/issues/1518)
* ship single-file executables built with bun
([#1524](https://github.com/agentclientprotocol/claude-agent-acp/issues/1524))
([e3f0b6c](e3f0b6c89c))
* start sessions in Auto mode by default
([#1533](https://github.com/agentclientprotocol/claude-agent-acp/issues/1533))
([7ea9eec](7ea9eec839))
* stream the output of v2 background commands
([#1512](https://github.com/agentclientprotocol/claude-agent-acp/issues/1512))
([53e3eec](53e3eec013))


### Bug Fixes

* honor configured fallback for unavailable restored modes
([#1520](https://github.com/agentclientprotocol/claude-agent-acp/issues/1520))
([8bbdea6](8bbdea6cc6))
* update to @anthropic-ai/claude-agent-sdk 0.3.318
([#1525](https://github.com/agentclientprotocol/claude-agent-acp/issues/1525))
([1d4418f](1d4418f80d))

---
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-05 22:07:42 +01:00
Ben Brandt
7ea9eec839 feat: start sessions in Auto mode by default (#1533)
Claude Code now starts interactive terminal and VS Code sessions in Auto
(v2.1.283+). Agent SDK runs with no `permissionMode` still start in
Manual ([docs](https://code.claude.com/docs/en/permission-modes)). Until
now the adapter followed the SDK default. ACP clients are interactive
editors, so this PR makes them start like the VS Code extension.

## How a session now starts

- **No mode configured:** with no `permissions.defaultMode`, or an
invalid one that settings resolution drops, a new session starts in
`auto`.
- **Model without Auto support:** that built-in Auto default starts in
Manual (`default`) with no warning, as Claude Code does. A user who
explicitly sets `"auto"` still gets the existing fallback to
`acceptEdits` with a warning.
- **`disableAutoMode: "disable"`:** the adapter ignored this setting
before. Now, set at the top level or under `permissions`, it removes
Auto from the mode list, the plan-exit options and mode switching, and
the session starts in `default`. The bundled CLI reads both spellings.
- **Unchanged:** an explicit `permissionMode`, a resumed transcript's
mode and a configured `defaultMode` behave as before.

## Limitations

- **HIPAA:** Claude Code starts organizations with the HIPAA
configuration in Manual. The SDK doesn't report that configuration (it
is not in `AccountInfo`, the `init` message or the model list), so those
sessions start in Auto here. This is noted in a code comment.
- **Mid-session setting changes:** like the bypass setting,
`disableAutoMode` is read when the session is created.

## Tests

- Updated the tests that expected the old default.
- Added cases for the built-in default, the Manual fallback on a model
without Auto, and both spellings of `disableAutoMode`.
- Regenerated the scenario snapshots for ACP v1 and v2. The 4
`origin-main` baseline files change only the mode value, from
`"default"` to `"auto"`.
2026-11-05 12:01:11 +00:00
Mark Tkachenko
a79d07324d feat: compact idle sessions before the prompt cache expires (#1522)
Claude Code compacts a large idle session shortly before its one-hour
prompt cache expires, but only in attended sessions. SDK sessions
(entrypoint `sdk-ts`) never arm that timer, so every return after an
hour re-writes the whole context to the cache.

The adapter now runs its own timer: 54 minutes after the last
main-thread request, if that request used the 1h cache, the context is
at least 200k tokens and the session is idle, it sends `/compact`
through the normal prompt path. It respects `idleCompaction: false`,
`autoCompactEnabled: false` and `DISABLE_COMPACT` /
`DISABLE_AUTO_COMPACT`. AIR gets `trigger: "idle"` in the compaction
`_meta`.

Closes #1454
Closes #1518
2026-11-04 20:04:11 +01:00
Ben Brandt
1d4418f80d fix: update to @anthropic-ai/claude-agent-sdk 0.3.318 (#1525)
Updates the Claude Agent SDK from 0.3.316 to 0.3.318, which bundles
Claude Code 2.1.318. Also updates `@anthropic-ai/sdk` (1.3.0 → 1.4.0),
`vitest` (5.0.5 → 5.1.0) and the transitive dependencies in the
lockfile.

## Changes from the changelogs

- **`session_writes_lost` (SDK 0.3.317):** a new system message that the
CLI sends as it exits, after the last result, when a full disk or
used-up quota kept it from saving the transcript or `~/.claude.json`. No
turn is open to carry a `session/update` then, so the adapter logs it,
as it does `mirror_error`. Without a case for it, the build fails on the
exhaustive switch.
- **TaskCreate `task.description` (SDK 0.3.317):** the plan now takes a
task's subject and description from the TaskCreate result, which holds
the task as saved. It differs from the input when Claude Code corrects a
malformed call or a PreToolUse hook changes it. This applies to the live
stream and to `session/load` replay.

## Reviewed, no change needed

- New `startup_failure_reason` values (`config_file_invalid`,
`initialize_not_first`) already reach the client as an internal error
with the CLI's own message.
- `onFailure: "block"` on hook matchers: none of the adapter's hooks
guard permissions, and a failed Edit/Write preview must not block the
edit.
- Removed `file_unchanged` Read output, optional `status` in
`PushNotificationInput`, and Grep/Glob `timedOut`: nothing in the
adapter reads these.
- `workingDirectoriesAsGiven`: it makes Claude Code not follow links at
the listed working directories. Listing the ACP `cwd` would put every
file of a symlinked workspace outside the working directories. Hosts
that want it can already pass it through
`_meta.claudeCode.options.settings`.
- The comments on the `getSettings` and `unanswered` type workarounds
now name 0.3.318; both are still missing from the SDK's types.

## Not updated

TypeScript stays on 6.0.3: typescript-eslint 8.72.1 requires `typescript
<6.1.0`, so 7.0.2 has to wait for a typescript-eslint release that
supports it.

## Testing

`npm run build`, `npm run check` and `npm run test:run` pass. New tests
cover both changes. `npm run test:integration` was not run.
2026-11-04 12:50:19 +00:00
Ben Brandt
21d24f9e8b ci: build the executables with the latest Bun (#1526)
Follow-up to #1524. The last commit on that branch didn't make it into
the merge.

- `binaries.yml` installs `bun-version: latest` instead of pinning
`1.4.4`. The compiled executable contains the Bun runtime, so the pin
also fixed the runtime we ship. But nothing checks for new Bun releases,
so the pin would just go stale. Each build is smoke-tested on its own
platform before it uploads, so a Bun release that breaks the executable
fails that job instead of shipping. npm is unaffected, and a re-run
fixes it once Bun does. If a Bun version ever regresses past the smoke
test, we pin it then.
- Each build job writes `Built <target> with Bun x.y.z` to its step
summary, so it's visible which Bun built each release's archives.
- Re-wraps an overlong line in the file's header comment.

`ci:` because nothing published changes until the next release builds
with it.
2026-11-04 12:50:03 +00:00
Ben Brandt
e3f0b6c89c feat: ship single-file executables built with bun (#1524)
Brings back the standalone executables that #525 and #572 turned off.
npm publishing is unchanged. Each stable release also attaches 8
archives (Linux glibc and musl, macOS, Windows, on x64 and arm64) to its
GitHub release.

## Why it works now

The old executables ran the SDK's JS CLI from inside Bun's `$bunfs`, and
it couldn't reach its `vendor/` files there. The SDK's CLI is now a
native binary shipped in a per-platform npm package. So each archive
ships that binary as `claude` next to `claude-agent-acp`, and the agent
runs it from its own directory. Nothing is embedded or extracted, and
the agent has no `node_modules` to resolve.

The first commit embedded the CLI and extracted it with the SDK's
`@anthropic-ai/claude-agent-sdk/extract` helper. That works, but the OS
can only run a real file, and Bun-embedded files only exist inside the
agent process. So it wrote a second ~300 MB copy to `/tmp` on first run,
once per version. Shipping the CLI next to the executable avoids that,
and the download size is the same (~180 MB `.tar.gz`).

Claude Code's helper tools work too. Native Claude Code has no Grep or
Glob tools: its Bash shell wraps `grep` and `find` in functions that run
the ugrep and bfs built into the binary at `$CLAUDE_CODE_EXECPATH`. That
variable points at the `claude` shipped next to the agent.

## Changes

- `src/claude-cli.ts` now owns `claudeCliPath()`, moved out of
`acp-agent.ts`. `useBundledClaudeCli(dir)` makes it return
`<dir>/claude[.exe]`. If that file is missing, it throws an error saying
to keep `claude` next to `claude-agent-acp` or set
`CLAUDE_CODE_EXECUTABLE`; before, the user got a bare ENOENT.
`CLAUDE_CODE_EXECUTABLE` still wins.
- `src/binary-entry.ts` is the executable's entry. It registers the
directory of `realpath(process.execPath)`, so a symlink in
`/usr/local/bin` works, then imports the agent. It's left out of the npm
tarball.
- `scripts/build-binary.mjs` (`npm run build:binary`) runs `bun build
--compile` for a target and copies that target's `claude` next to the
output.
- `scripts/smoke-binary.mjs` (`npm run smoke:binary`) runs:
  - `--version` and `--cli --version`
  - a symlinked executable
  - the missing-`claude` error
  - `initialize` over v1 and v2
- the `terminal-auth` login command, run with `--version` (checks that
no `$bunfs` path leaks into it)

  With `--full` it also drives a real session:
- Bash `grep`/`find`, checking that they ran Claude's embedded ugrep/bfs
  - Read on text and on a PNG
  - Edit
  - a subagent
  - `session/cancel` during a running command
  - `session/list`
  - `session/load` in a new process
  - `session/fork` and `session/delete`
- `src/agent-command.ts` builds the `terminal-auth` login command, which
reruns the agent. Under Node that's `node <script> ...flags`. In a
compiled executable, `argv[1]` is a path inside the executable
(`/$bunfs/root/claude-agent-acp`), so it's dropped. Passing it back, as
the old `argv.slice(1)` did, handed it to the Claude CLI as an argument
and broke `auth login`.
- `.github/workflows/binaries.yml` builds and smoke-tests each target on
its own platform's runner; the musl targets are tested in an Alpine
container. CI runs it on every PR, using the basic smoke test, since CI
has no Claude login. On a stable release, `publish.yml` attaches the
archives (same names as before). These jobs don't gate the npm publish,
and previews get no executables. On stable releases the registry
dispatch waits for `upload-binaries`, so the archives (and the digests
the registry reads) exist first. It only waits: a failed upload doesn't
stop the dispatch.

## Testing

I also ran the whole unit suite on Bun's runtime (`bun --bun vitest
run`). 3,573 of 3,575 tests pass. The two failures are test artifacts:
Bun's `os.homedir()` ignores a `HOME` changed at runtime, and one
steer-and-cancel test asserts before the mock delivers its last idle. An
idle-accounting trace is identical on both runtimes.


On macOS arm64 with Bun 1.4.4, `smoke:binary --full` passes all checks.
`CLAUDE_CODE_EXECPATH` pointed at the shipped `claude`, and nothing was
written to the temp directory. The basic smoke test also passes from an
unpacked archive outside the repo with `PATH=/usr/bin:/bin`, and `node
dist/index.js` passes the full run. On Linux, Windows and Intel macOS,
only the basic smoke test runs, in this PR's CI.
2026-11-04 11:00:25 +00:00
Slapoguzov Alexander
8bbdea6cc6 fix: honor configured fallback for unavailable restored modes (#1520)
A restored transcript mode such as `dontAsk` is recognized by the Claude
SDK but absent from the ACP mode catalog. It previously reached
initialization and was forced to Manual, skipping a supported
`permissions.defaultMode` from settings.

Use the same catalog for restored-mode selection and initialization.
Supported restored modes retain precedence; an unavailable restored mode
falls back to an available configured mode, then Manual. Explicit
initial modes, bypass restrictions, model-specific Auto handling, and
new-session configuration behavior remain unchanged.

Validation: TypeScript build, ESLint, Prettier, and 243 focused tests
pass. Added regressions cover helper selection plus v1 and v2 responses;
nine checks fail against unchanged production code.

The full local suite **FAILED**: 3,549 passed, 20 failed, 30 skipped.
Four selected failures also reproduce on unchanged base: three
session-index cases (watcher setup, live notification, SDK path-list
parity) and one background-work case. The remaining 16 failures were not
individually classified. No test timeouts or product behavior were
changed to suppress them. Live CLI and IDE permission-enforcement
acceptance remain unverified.

Co-authored-by: Codex <codex@openai.com>
2026-11-03 10:40:14 +01:00
Ben Brandt
1e986b526d refactor: give each session one owner of its background work (#1517)
Follow-up to #1510, #1511 and #1512. Clients receive exactly what they
did before.

## Why

The v2 background work grew as about 14 free functions taking `(session,
sessionId, …)` and reaching into six `Session` fields. Four of them it
kept itself: the task set, terminals, tool calls, and
`reportsBackground`. Three belong to other modules:
`liveBackgroundTasks`, the subagent map, and `toolUseCache`. The repo
guidelines ask for one module that owns the state.

## What changed

- **One owner per session.** `SessionBackgroundWork`, stored on
`Session.background`, owns the task set, terminals, tool calls, output
tails, and whether the client gets the lists. It has one method per
event: a Bash result, tool results, a task's end or pause, a new task
set, and a closed query.
- **The reporter only filters.** The object builds each session's
report, so `BackgroundWorkReporter` only drops unchanged lists and
forwards the rest.
- **No reference back to the session.** A recreated session (sign-out
respawn, provider change) gets a new `Session` under the same id, so an
old consumer must not reach the new one. Callers pass in the state other
modules own.
- **No `toolUseCache` dependency.** Whether a task's launching call has
returned used to be judged by the call still being in `toolUseCache`,
which Claude Code prunes at the tool's result. The object now records
returned calls itself, capped at 1024 like the other per-session sets.
- **Stricter typing on the output path.** Typing the terminals map
properly exposed that the parsed `outputFilePath` was loosely typed, so
it is now checked to be a string.
- **Test doubles.** The hand-built `Session` doubles in
`acp-agent.test.ts` and `session-doubles.ts` now carry the new required
field.

## Tests

- The existing suite passes unchanged.
- One new v2 test covers a task whose `task_started` arrives after its
call returned, so it is listed as `_claude/task`. A first version of
this refactor recorded "returned" per task and silently missed that
case. The test fails against that version, and against listing tasks
before their call returns.

`npm run check`, `npm run build` and `npm run test:run` pass (3556
passed, 30 skipped).
2026-11-02 19:52:25 +01:00
Ben Brandt
53e3eec013 feat: stream the output of v2 background commands (#1512)
Follow-up to #1510 and #1511, phase 2b of the v2 [Background Work
RFD](https://agentclientprotocol.com/rfds/v2/background-work).

Claude Code streams no output of a Bash command that went to the
background: it writes the output to the task's output file. So the
command's terminal showed only the Bash result's placeholder ("Command
running in background with ID …") until the command exited. The v2 spec
says the Agent **SHOULD** keep a listed terminal's output current
whenever it can read the process output.

## What a v2 client gets

- **Snapshot:** the file's first text replaces the placeholder as a
`terminal_output` snapshot.
- **Chunks:** later text is appended as `terminal_output_chunk`s.
- **Task end:** the rest of the file is read before the terminal's
`exitStatus`, as the spec asks.
- **Query close:** reading stops.

## Design

- New `src/background-output.ts` (`BackgroundOutputTail`) follows one
file:
- It polls every 500 ms, because watching a file is not reliable on
every platform and a missing file just means nothing was written yet.
- It reads at most 256 KiB per read, so a burst of output doesn't block
the event loop. Each read is sent before the next, including when
reading to the end at the task's end.
- Past 1 MiB of unread output (a command writing faster than the polls
read, about 512 KiB/s), it skips to the file's last 1 MiB and sends it
as a replacing snapshot, so the terminal shows the latest output without
a gap in it. A file that shrinks is read again from its start, also as a
snapshot.
- Its timer is unref'd, so a long-running command doesn't keep the agent
process alive.
- A `StringDecoder` holds a UTF-8 character split across reads until its
last byte is written.
  - Reads are serialised.
- `background-work.ts` starts the tail once the Bash result's own
updates are sent, so the first output replaces the placeholder rather
than coming before it. It finishes the tail before the terminal's exit
and stops it when the query closes. `acp-agent.ts` only gets wiring.
- The output path comes from the Bash result's text, the only place the
SDK gives it before the task ends. The existing
`asyncTaskOutputFilePath` parser only accepts an absolute path ending in
`tasks/<taskId>.output` for this task. Without a path, the terminal
keeps the placeholder as before.
- v2 only (`reportsBackground`), like the rest of the background work.
v1 clients that render terminals through the `_meta` extension could use
it too. That is a small follow-up, but it changes what v1 clients
receive.

## Tests

- Nine unit tests for `BackgroundOutputTail`:
  - the replacing first read, and appends
  - a file that appears later, and a file that never appears
  - a UTF-8 character split across reads
  - finishing, an empty output, and stopping
  - a backlog past 1 MiB, and a file that shrinks
- One v2 test over a real connection with a real output file. It checks
that the placeholder is replaced, later output arrives as chunks, and
the rest arrives before the exit.
- I checked by mutation that removing the read-before-exit, the
replacing snapshot, the UTF-8 decoder, the backlog cap or the shrink
restart each fails a test.

`npm run check`, `npm run build` and `npm run test:run` pass (3555
passed, 30 skipped).
2026-11-02 19:37:49 +01:00
96 changed files with 4301 additions and 971 deletions

125
.github/workflows/binaries.yml vendored Normal file
View File

@@ -0,0 +1,125 @@
# Builds the standalone executables (scripts/build-binary.mjs) and smoke-tests
# each one on a runner of its own platform. Each archive holds
# `claude-agent-acp` and the Claude CLI (`claude`) it runs from its directory.
# CI calls this to check that every target still builds and starts; the
# release calls it with `upload: true` and attaches the archives to the GitHub
# release.
name: Binaries
on:
workflow_call:
inputs:
upload:
description: Upload each archive as a workflow artifact for the release job.
type: boolean
default: false
permissions:
contents: read
jobs:
build:
name: Binary (${{ matrix.target }})
runs-on: ${{ matrix.runner }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- target: bun-linux-x64
runner: ubuntu-latest
archive: claude-agent-acp-linux-x64.tar.gz
- target: bun-linux-arm64
runner: ubuntu-24.04-arm
archive: claude-agent-acp-linux-arm64.tar.gz
# npm skips the musl package on a glibc host, so these are
# cross-built and smoke-tested in an Alpine container.
- target: bun-linux-x64-musl
runner: ubuntu-latest
archive: claude-agent-acp-linux-x64-musl.tar.gz
musl: true
- target: bun-linux-arm64-musl
runner: ubuntu-24.04-arm
archive: claude-agent-acp-linux-arm64-musl.tar.gz
musl: true
- target: bun-darwin-x64
runner: macos-15-intel
archive: claude-agent-acp-darwin-x64.zip
- target: bun-darwin-arm64
runner: macos-latest
archive: claude-agent-acp-darwin-arm64.zip
- target: bun-windows-x64
runner: windows-latest
archive: claude-agent-acp-windows-x64.zip
- target: bun-windows-arm64
runner: windows-11-arm
archive: claude-agent-acp-windows-arm64.zip
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1
with:
node-version: "lts/*"
check-latest: true
cache: "npm"
# Unpinned: each build is smoke-tested on its platform before it uploads,
# so a Bun release that breaks the executable fails here instead of
# shipping. Pin a version if one ever regresses past the smoke test.
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: latest
- run: npm ci
- name: Install the musl Claude CLI package
if: ${{ matrix.musl }}
run: |
version="$(node -p "require('./node_modules/@anthropic-ai/claude-agent-sdk/package.json').version")"
npm install --no-save --force "@anthropic-ai/claude-agent-sdk-linux-${RUNNER_ARCH,,}-musl@$version"
- name: Build
run: |
echo "Built \`${{ matrix.target }}\` with Bun $(bun --version)" >> "$GITHUB_STEP_SUMMARY"
node scripts/build-binary.mjs --target ${{ matrix.target }}
- name: Smoke test
if: ${{ !matrix.musl }}
run: node scripts/smoke-binary.mjs
# Claude Code's Alpine requirements: libgcc and libstdc++.
- name: Smoke test (Alpine)
if: ${{ matrix.musl }}
run: |
docker run --rm -v "$PWD:/work" -w /work node:lts-alpine \
sh -c 'apk add --no-cache libgcc libstdc++ >/dev/null && node scripts/smoke-binary.mjs'
- name: Archive
if: ${{ inputs.upload }}
env:
ARCHIVE: ${{ matrix.archive }}
run: |
cd dist-bin
if [[ "$ARCHIVE" == *.zip ]]; then
if [[ "$RUNNER_OS" == "Windows" ]]; then
pwsh -Command "Compress-Archive -Path claude-agent-acp.exe,claude.exe -DestinationPath ../$ARCHIVE"
else
zip "../$ARCHIVE" claude-agent-acp claude
fi
else
tar czf "../$ARCHIVE" claude-agent-acp claude
fi
- name: Upload
if: ${{ inputs.upload }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ${{ matrix.archive }}
path: ${{ matrix.archive }}
retention-days: 7
if-no-files-found: error

View File

@@ -44,3 +44,7 @@ jobs:
- name: Run tests
run: npm run test:run
binaries:
name: Binaries
uses: ./.github/workflows/binaries.yml

View File

@@ -88,6 +88,38 @@ jobs:
npm publish
echo "version=$version" >> "$GITHUB_OUTPUT"
# Standalone executables for the GitHub release, stable releases only. They
# don't gate the npm publish: a failed build leaves the release without
# binaries, and re-running the failed jobs attaches them.
build-binaries:
name: Build binaries
needs: [release-please]
if: ${{ needs.release-please.outputs.release_created == 'true' }}
# The called workflow only reads the repository; upload-binaries does the write.
permissions:
contents: read
uses: ./.github/workflows/binaries.yml
with:
upload: true
upload-binaries:
name: Attach binaries to the release
runs-on: ubuntu-latest
needs: [release-please, build-binaries]
permissions:
contents: write
timeout-minutes: 15
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
path: artifacts
merge-multiple: true
- name: Upload to the release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.release-please.outputs.tag_name }}
run: gh release upload "$TAG" artifacts/* --clobber --repo "$GITHUB_REPOSITORY"
# Same shape as publish-npm. Tagging is a separate job, so nothing here needs
# write access to the repository.
publish-npm-preview:
@@ -199,7 +231,11 @@ jobs:
echo "Tagged \`v$VERSION\` at \`$SHA\`" >> "$GITHUB_STEP_SUMMARY"
trigger-registry-update:
needs: [publish-npm, publish-npm-preview]
# upload-binaries too, so a stable release's archives are attached before the
# registry reads the release: it takes each archive's sha256 from the release
# assets when it bumps the version. It only waits; a failed or skipped upload
# (every preview) doesn't stop the dispatch.
needs: [publish-npm, publish-npm-preview, upload-binaries]
# Both channels dispatch this: the registry has its own handling for preview
# versions. The two never fire together — a release merge publishes stable and
# skips the preview, and every other push does the reverse — so the registry sees

3
.gitignore vendored
View File

@@ -3,4 +3,5 @@ dist
.DS_Store
.claude/*.local.*
CLAUDE.local.md
.idea/
.idea/
dist-bin

View File

@@ -1,3 +1,3 @@
{
".": "0.100.0"
".": "0.101.0"
}

View File

@@ -1,5 +1,21 @@
# Changelog
## [0.101.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.100.0...v0.101.0) (2026-11-05)
### Features
* compact idle sessions before the prompt cache expires ([#1522](https://github.com/agentclientprotocol/claude-agent-acp/issues/1522)) ([a79d073](https://github.com/agentclientprotocol/claude-agent-acp/commit/a79d07324dc15808c94a889a3b60615cca5a9fbd)), closes [#1454](https://github.com/agentclientprotocol/claude-agent-acp/issues/1454) [#1518](https://github.com/agentclientprotocol/claude-agent-acp/issues/1518)
* ship single-file executables built with bun ([#1524](https://github.com/agentclientprotocol/claude-agent-acp/issues/1524)) ([e3f0b6c](https://github.com/agentclientprotocol/claude-agent-acp/commit/e3f0b6c89c783939e6ca8c19c220c21b58263f46))
* start sessions in Auto mode by default ([#1533](https://github.com/agentclientprotocol/claude-agent-acp/issues/1533)) ([7ea9eec](https://github.com/agentclientprotocol/claude-agent-acp/commit/7ea9eec8392a43191b25a5c3636c181cc7eb42f3))
* stream the output of v2 background commands ([#1512](https://github.com/agentclientprotocol/claude-agent-acp/issues/1512)) ([53e3eec](https://github.com/agentclientprotocol/claude-agent-acp/commit/53e3eec0134209654f53c46f319aaac851b8fff0))
### Bug Fixes
* honor configured fallback for unavailable restored modes ([#1520](https://github.com/agentclientprotocol/claude-agent-acp/issues/1520)) ([8bbdea6](https://github.com/agentclientprotocol/claude-agent-acp/commit/8bbdea6cc62a7ae080cb73ff476ed28ad7574018))
* update to @anthropic-ai/claude-agent-sdk 0.3.318 ([#1525](https://github.com/agentclientprotocol/claude-agent-acp/issues/1525)) ([1d4418f](https://github.com/agentclientprotocol/claude-agent-acp/commit/1d4418f80daf14c29cecc5b23286f52c7f0402ec))
## [0.100.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.99.1...v0.100.0) (2026-11-02)

View File

@@ -36,6 +36,23 @@ To try changes that have landed on `main` but are not released yet, install from
npm install @agentclientprotocol/claude-agent-acp@preview
```
### Standalone executables
Each release attaches archives for Linux (glibc and musl), macOS and Windows, on
x64 and arm64, to its [GitHub release](https://github.com/agentclientprotocol/claude-agent-acp/releases).
They need no Node.js install. Each holds two files: `claude-agent-acp`, and the
Claude CLI (`claude`) it runs. Keep them in the same directory; a symlink to
`claude-agent-acp` elsewhere works. To use a Claude CLI you already have
instead, set `CLAUDE_CODE_EXECUTABLE`. On Alpine, install `libgcc` and
`libstdc++` first.
To build one for your machine (needs [Bun](https://bun.sh)) and check it:
```sh
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

View File

@@ -7,7 +7,9 @@ keeps a single release PR open, titled `chore(main): release X.Y.Z` and labelled
`autorelease: pending`.
Merging that PR is what releases. It tags `vX.Y.Z`, creates the GitHub release,
publishes to npm, and dispatches a version update to the agent registry.
publishes to npm, dispatches a version update to the agent registry, and
attaches the [standalone executables](../README.md#standalone-executables) to
the GitHub release. Previews get no executables.
There is no manual release button, and versions are never typed in by hand: the
version is an output of the commit history, not an input.
@@ -45,6 +47,9 @@ gh release view "v<version>"
npm view "@agentclientprotocol/claude-agent-acp@<version>"
```
The release should list 8 `claude-agent-acp-*` archives. They are attached a
few minutes after the release is created, once every platform has built.
## Preview releases
Every push to `main` that is not a release merge publishes a preview from the
@@ -203,6 +208,13 @@ npm versions are immutable. If the package already published and only the
registry update failed, pass `-f publish_npm=false` so the run skips publishing
and only re-dispatches the registry update.
### The release has no executables
`build-binaries` and `upload-binaries` don't gate npm, so a failed build only
leaves the release without archives. Re-run the failed jobs of that publish run
(`gh run rerun <run-id> --failed`); the upload uses `--clobber`, so archives
already attached are replaced rather than duplicated.
### A preview published but the commit was not tagged
Only the publish is irreversible, so re-run just the tag job:

View File

@@ -1679,14 +1679,14 @@ For AIR, both forms carry `_meta.jetbrains.air.contextCompaction`:
}
```
| Field | Meaning |
| ------------ | --------------------------------------------------------------------- |
| `version` | Must equal `1`. |
| `trigger` | `manual` or `automatic`. |
| `preTokens` | The context size before the compaction. |
| `postTokens` | The context size after the compaction. |
| `durationMs` | The duration of the compaction. |
| `error` | The error of a failed compaction. Only the tool call form carries it. |
| Field | Meaning |
| ------------ | -------------------------------------------------------------------------------------------------------- |
| `version` | Must equal `1`. |
| `trigger` | `manual`, `automatic`, or `idle`: the adapter compacted an idle session before its prompt cache expired. |
| `preTokens` | The context size before the compaction. |
| `postTokens` | The context size after the compaction. |
| `durationMs` | The duration of the compaction. |
| `error` | The error of a failed compaction. Only the tool call form carries it. |
The standard `toolCallId` or `compactionId` and the `status` own the identity and the phase.
The `compaction_update` form puts the error in the standard `error` field.

176
package-lock.json generated
View File

@@ -1,16 +1,16 @@
{
"name": "@agentclientprotocol/claude-agent-acp",
"version": "0.100.0",
"version": "0.101.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@agentclientprotocol/claude-agent-acp",
"version": "0.100.0",
"version": "0.101.0",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "2.4.0",
"@anthropic-ai/claude-agent-sdk": "0.3.316",
"@anthropic-ai/claude-agent-sdk": "0.3.319",
"diff": "9.0.0",
"zod": "4.6.7"
},
@@ -18,7 +18,7 @@
"claude-agent-acp": "dist/index.js"
},
"devDependencies": {
"@anthropic-ai/sdk": "1.3.0",
"@anthropic-ai/sdk": "1.6.0",
"@eslint/js": "10.0.1",
"@tsconfig/node22": "22.0.6",
"@types/node": "26.6.6",
@@ -31,7 +31,7 @@
"globals": "17.14.0",
"prettier": "3.9.10",
"typescript": "6.0.3",
"vitest": "5.0.5"
"vitest": "5.1.0"
},
"engines": {
"node": ">=22"
@@ -53,22 +53,22 @@
}
},
"node_modules/@anthropic-ai/claude-agent-sdk": {
"version": "0.3.316",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.316.tgz",
"integrity": "sha512-mCMr0que5kIy80JslJub6WFDB2+GYRGWNeVgSZaBpkBwXrr4wnRMnnGHitkA2BEJ5QP5xWyc6YH/6do/edxCWg==",
"version": "0.3.319",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.319.tgz",
"integrity": "sha512-7X+sh4uZv/u4OL4I4cQvMr9N3NM6FDiuyk829hFylgbrnNwVyiGElWEDWGuO26zwMIxmgpKdPoDILoFIHh1uwA==",
"license": "SEE LICENSE IN README.md",
"engines": {
"node": ">=18.14.0"
},
"optionalDependencies": {
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.316",
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.316",
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.316",
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.316",
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.316",
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.316",
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.316",
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.316"
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.319",
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.319",
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.319",
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.319",
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.319",
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.319",
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.319",
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.319"
},
"peerDependencies": {
"@anthropic-ai/sdk": ">=0.93.0",
@@ -77,9 +77,9 @@
}
},
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": {
"version": "0.3.316",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.316.tgz",
"integrity": "sha512-1I1oJ/YGqq4pFQvJqycD2e8pbsUQLLR+DwEzG9jQAGCoSKkxP1TBfHpjSik/0byCixyzpYObrYXPC0PKBIR91g==",
"version": "0.3.319",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.319.tgz",
"integrity": "sha512-rNiAbMbRhgOM2CELsDTIq7q++sFVIZukTkgeswcncGbcX8STIgtf0XaZzKdRXtKK+v9ez+mQn8P4pb2Ka17wGA==",
"cpu": [
"arm64"
],
@@ -90,9 +90,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": {
"version": "0.3.316",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.316.tgz",
"integrity": "sha512-gzNncKrID/6yJIKCAuF/pY1kfygQZ4dU0J+MNFl0bU+iQW65ERoIyoghYVfrAQ/gH2fuvnQtWxa8LQNScHgNMA==",
"version": "0.3.319",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.319.tgz",
"integrity": "sha512-jM730Fl+cFotDnjF9cStIf+Xe57YdfDB3TXaSVVv8uVAXW4xoWFwIj2eDvmsciEEe5UMkKzjVqXu9kR21OrXng==",
"cpu": [
"x64"
],
@@ -103,9 +103,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": {
"version": "0.3.316",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.316.tgz",
"integrity": "sha512-+lm9XSw/em14yMZ41fbo80kfblbylfBQc9nYS6GgLoZVvgT3wSeEbv88v+mEK/jFw9g3VCH4ZI01sjNM34o7uw==",
"version": "0.3.319",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.319.tgz",
"integrity": "sha512-g2tpMsHLMQahI4bk5CIcMHAB/RwzxoPGmL5Q5G5MDir5WTrl71vulWbtLQISUPm/yFPRG97tdzZnCa5Uq1v/dw==",
"cpu": [
"arm64"
],
@@ -119,9 +119,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": {
"version": "0.3.316",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.316.tgz",
"integrity": "sha512-1qqaEdsAs6eipEQQ4bMAWiwDjIouyA/YN5KQWS0/+kuPb+GKS8rUVW2dty2elV703hsMovb4IhMP4cv5K7R9YA==",
"version": "0.3.319",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.319.tgz",
"integrity": "sha512-8KDZQJAS2CEb5aGAvuXkpyHMc9TOM/zYTw2iKmbbVi4im0atw93EtJPapYyp0g/lQI/Xk8jMYdWU6Zlv9EWjLw==",
"cpu": [
"arm64"
],
@@ -135,9 +135,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": {
"version": "0.3.316",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.316.tgz",
"integrity": "sha512-+csYyGkHziNp4It+ZykqTmXRZBuNrieTiBnji+zwAxNmLLCWz8xEWjNmwhaLPIMpUiaYREkDhZXt/AS6xXta5w==",
"version": "0.3.319",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.319.tgz",
"integrity": "sha512-uBAZA8x0hd5gVifyL5Lo73dVmi9xZeJ6jro2NO0al6yyWaw/6UL9xbsVh9lOhLFOmYIdSbQ2j4zAfnvG/t5F3Q==",
"cpu": [
"x64"
],
@@ -151,9 +151,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": {
"version": "0.3.316",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.316.tgz",
"integrity": "sha512-pMG9MRkjxMMbIxpwg/cKz6JorNjZtJOA9l/fifRQ/A01zGTKrRIaSCWoGg+Asmc64QBOa3aA4vzE/od9B1R0Tw==",
"version": "0.3.319",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.319.tgz",
"integrity": "sha512-XkKYj3c1jVFjLATrZr4YTsmS5AIb5s2iphF9X670EcVQqDCCGIKKTVNLLg9mg1e7D+2xX8FU0XNlOJatWfpBeA==",
"cpu": [
"x64"
],
@@ -167,9 +167,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": {
"version": "0.3.316",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.316.tgz",
"integrity": "sha512-Xj4iTS8NsB6isGhnZEnaIXIpUcSspv/lyIVnfPZjVGwuttaVHjq4IxfDopsPaVsPuTKd+L7DLAaBram1k85U2g==",
"version": "0.3.319",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.319.tgz",
"integrity": "sha512-YoP0lYHJCziENFCotHQq9qd/gaJrwUNkAcnZyVjXxg1OPmtKuyw31aG60s4cWXRDXXGm/m1siNSQaGGfAYBvrw==",
"cpu": [
"arm64"
],
@@ -180,9 +180,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": {
"version": "0.3.316",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.316.tgz",
"integrity": "sha512-FzQA1zHbYJTtLR9nn1McwkGYeiEmroTmYjIg7xZqo5eweJ8U/DESZKR/QdfF04MXe2DF23oLL8WHjEa+WxbGQw==",
"version": "0.3.319",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.319.tgz",
"integrity": "sha512-UC27x+8gfCS2OjNGJWrueP0gLOOKsLEsJ/kg0p6aa8GToZcJYjJ8KeAapHHRYMUAIB6Sarqe49Itm5P1wswCcQ==",
"cpu": [
"x64"
],
@@ -193,9 +193,9 @@
]
},
"node_modules/@anthropic-ai/sdk": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-1.3.0.tgz",
"integrity": "sha512-k7z9BlLLPxfZH+ov7dS6EnaN/wiZeV8YNT1LZfrpAHIy7oY7pHr4UzirF9f2OlHtFEnRBOX+AZQ91Y5fK+TPTw==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-1.6.0.tgz",
"integrity": "sha512-XqiO/46n8S6So8jQ/6/uNCkHntyW6QHHs1gZ+hlwTx1fWDr929g4VmwIXfdMVk7SuBWNdQvAlwyi1G3t6SEIuQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api": "^1.4.1",
@@ -215,9 +215,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.29.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz",
"integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==",
"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==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -267,9 +267,9 @@
}
},
"node_modules/@eslint-community/regexpp": {
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
"version": "4.12.3",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.3.tgz",
"integrity": "sha512-R76UKbvsqI68DJJVTVyqFKqU98CN4+B+acxNfEA2IFarSzP0SjtKf/3+fFiXzTyv5Fj3PxLkGHe2wnVWWMxobQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -843,9 +843,9 @@
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.1.0.tgz",
"integrity": "sha512-K+NU2HDCEem4sClgHH4ZZXwdJeNFkJo+hRk479DIwdIviAPN2hFJH1KHHwg8CxHQXvi0M5XY4deTXINfIiSG6w==",
"dev": true,
"license": "MIT",
"peer": true
@@ -1146,14 +1146,14 @@
}
},
"node_modules/@vitest/mocker": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.5.tgz",
"integrity": "sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.1.0.tgz",
"integrity": "sha512-5umB9/jlWcqVS6fsrFmv5Gdnpu6wAUio/lEOwAmvB7v+lFo/IW2B9FhAD0p19GCULTgMzLCdUmuCDCnAzWgppQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "0.3.31",
"@vitest/spy": "5.0.5",
"@vitest/spy": "5.1.0",
"estree-walker": "^3.0.3",
"magic-string": "^1.4.3"
},
@@ -1174,9 +1174,9 @@
}
},
"node_modules/@vitest/spy": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.5.tgz",
"integrity": "sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.1.0.tgz",
"integrity": "sha512-2DPO5OMHWKrBngC+fCmTQqemNF0gidRhp3lxbBmRhW+M1XJrfgPpqNqWVevXtn2f2ufD5ZaR1B6B641aAlcX5A==",
"dev": true,
"license": "MIT",
"funding": {
@@ -1945,9 +1945,9 @@
}
},
"node_modules/express-rate-limit": {
"version": "8.7.2",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.2.tgz",
"integrity": "sha512-U1ScocxDD9WI09LaBVypvNkOtNR5yWAmZLkuKJdAzIchA4YKFYF6GT5IKER7epG58oXtjvGlV0/6ii0F83EyZg==",
"version": "8.7.3",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.3.tgz",
"integrity": "sha512-zPLqDI2f0gV2WmGnacLf9XSSqn4VldRyP5LCzOwx0BKH40mO6+2PCFkoSsRO2jePx87/Kf6NUF6P2QOTKXHbQA==",
"license": "MIT",
"peer": true,
"dependencies": {
@@ -2270,9 +2270,9 @@
}
},
"node_modules/hono": {
"version": "4.13.16",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.16.tgz",
"integrity": "sha512-AOLOEhvrV+iH92xXCnFr/5+3KjPKcI3xP63XJtX5spAnkznPdHijlIsUTpi7/9EhWqrTCekw5c5zh1K+YsVw7A==",
"version": "4.13.17",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.17.tgz",
"integrity": "sha512-vZIcbkvjOR5hHRWERbGkr8fGfF0GGM1ooYkuH448iO3b2AdVEr3AXZSw4Z3Kn+1L5z0CVoVeyV29VGYmFh05kQ==",
"license": "MIT",
"peer": true,
"engines": {
@@ -2352,9 +2352,9 @@
"peer": true
},
"node_modules/ip-address": {
"version": "10.7.5",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.5.tgz",
"integrity": "sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA==",
"version": "10.7.6",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.6.tgz",
"integrity": "sha512-00LYkM6byILa9eQIx6qoaXCM40xTLd56AZ4vyaPW3YTyb49VZYPo5xsiAq4tuQ54GH4R7gVdYScJ1B7B77PuTg==",
"license": "MIT",
"peer": true,
"engines": {
@@ -2408,9 +2408,9 @@
"license": "ISC"
},
"node_modules/jose": {
"version": "6.2.15",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.15.tgz",
"integrity": "sha512-qdF6ewcZARtef63fJ9AKKZPgxw+t7USH6HLXYcD4CrdmSvDp79rpJSPFEKc90QBeGS63u1KQtN2MZQHC0yDIzA==",
"version": "6.2.16",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.16.tgz",
"integrity": "sha512-4LSJpbuBuPdGrONCdZUy0xnxtLf8eEz+EY0SMXdHQEBU7bgUHDVblU/JAjryv3wID9/LK7/YuwFwq+AQNfMscQ==",
"license": "MIT",
"peer": true,
"funding": {
@@ -3334,9 +3334,9 @@
"peer": true
},
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"version": "7.8.6",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz",
"integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==",
"dev": true,
"license": "ISC",
"bin": {
@@ -3699,17 +3699,17 @@
}
},
"node_modules/vite": {
"version": "8.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz",
"integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==",
"version": "8.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.6.tgz",
"integrity": "sha512-UI7dK29NnS54Fk+mmo7o+jI6U8Fp7hDl+X5Q1k45WH+d4dSlbLVUETGQ+hfkNgvLQGyoElPj837nZyv4MBc0VA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"lightningcss": "^1.33.0",
"picomatch": "^4.0.7",
"postcss": "^8.5.30",
"rolldown": "~1.2.14",
"postcss": "^8.5.32",
"rolldown": "~1.2.16",
"tinyglobby": "^0.2.17"
},
"bin": {
@@ -3778,14 +3778,14 @@
}
},
"node_modules/vitest": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.5.tgz",
"integrity": "sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-5.1.0.tgz",
"integrity": "sha512-MzmNFnrwa8v8lF1bLxw3UJ4EREwVOVb75VqJZmUDRjJ6EluK/x9th7qTbaeDk41WxE1GxWqmTXcdsfobfH5sOA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/mocker": "5.0.5",
"@vitest/mocker": "5.1.0",
"chai": "^6.3.0",
"es-module-lexer": "^2.3.2",
"expect-type": "^1.4.0",
@@ -3811,12 +3811,12 @@
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^22.0.0 || >=24.0.0",
"@vitest/browser-playwright": "5.0.5",
"@vitest/browser-preview": "5.0.5",
"@vitest/browser-playwright": "5.1.0",
"@vitest/browser-preview": "5.1.0",
"@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0",
"@vitest/coverage-istanbul": "5.0.5",
"@vitest/coverage-v8": "5.0.5",
"@vitest/ui": "5.0.5",
"@vitest/coverage-istanbul": "5.1.0",
"@vitest/coverage-v8": "5.1.0",
"@vitest/ui": "5.1.0",
"happy-dom": "*",
"jsdom": "*",
"vite": "^6.4.0 || ^7.0.0 || ^8.0.0"

View File

@@ -3,7 +3,7 @@
"publishConfig": {
"access": "public"
},
"version": "0.100.0",
"version": "0.101.0",
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
"main": "dist/lib.js",
"types": "dist/lib.d.ts",
@@ -21,12 +21,15 @@
"files": [
"dist/",
"!dist/tests/",
"!dist/binary-entry.*",
"README.md",
"LICENSE",
"package.json"
],
"scripts": {
"build": "tsc",
"build:binary": "node scripts/build-binary.mjs",
"smoke:binary": "node scripts/smoke-binary.mjs",
"start": "node dist/index.js",
"dev": "npm run build && npm run start",
"example:simple-client": "node --experimental-strip-types --disable-warning=ExperimentalWarning examples/simple-client.ts",
@@ -66,12 +69,12 @@
},
"dependencies": {
"@agentclientprotocol/sdk": "2.4.0",
"@anthropic-ai/claude-agent-sdk": "0.3.316",
"@anthropic-ai/claude-agent-sdk": "0.3.319",
"diff": "9.0.0",
"zod": "4.6.7"
},
"devDependencies": {
"@anthropic-ai/sdk": "1.3.0",
"@anthropic-ai/sdk": "1.6.0",
"@eslint/js": "10.0.1",
"@tsconfig/node22": "22.0.6",
"@types/node": "26.6.6",
@@ -84,6 +87,6 @@
"globals": "17.14.0",
"prettier": "3.9.10",
"typescript": "6.0.3",
"vitest": "5.0.5"
"vitest": "5.1.0"
}
}

92
scripts/build-binary.mjs Normal file
View File

@@ -0,0 +1,92 @@
#!/usr/bin/env node
// Builds an executable of the agent with `bun build --compile` and copies the
// target platform's Claude CLI next to it. Both files go in the release
// archive: the executable runs the CLI from its own directory.
//
// node scripts/build-binary.mjs [--target bun-linux-x64-musl] [--outfile path]
//
// --target defaults to the host. Cross-compiling needs the target's SDK
// platform package installed; the script prints the install command when it
// is missing.
import { spawnSync } from "node:child_process";
import { chmodSync, copyFileSync, readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { parseArgs } from "node:util";
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
function hostTarget() {
const os = process.platform === "win32" ? "windows" : process.platform;
// Node linked against glibc reports its version; musl builds don't.
const musl =
process.platform === "linux" && !process.report.getReport().header.glibcVersionRuntime;
return `bun-${os}-${process.arch}${musl ? "-musl" : ""}`;
}
const { values } = parseArgs({
options: {
target: { type: "string", default: hostTarget() },
outfile: { type: "string" },
},
});
const target = values.target;
// Bun targets look like `bun-<os>-<arch>` with optional `-musl`, `-baseline` or
// `-modern` suffixes; the SDK names its packages `<platform>-<arch>[-musl]`.
const match = /^bun-(linux|darwin|windows)-(x64|arm64|aarch64)(?:-.+)?$/.exec(target);
if (!match) {
console.error(
`Unsupported target "${target}". Expected e.g. bun-linux-x64, bun-darwin-arm64, bun-windows-x64.`,
);
process.exit(1);
}
const [, os, rawArch] = match;
const platform = os === "windows" ? "win32" : os;
const arch = rawArch === "aarch64" ? "arm64" : rawArch;
const musl = target.split("-").includes("musl");
const exe = os === "windows" ? ".exe" : "";
const cliPackage = `@anthropic-ai/claude-agent-sdk-${platform}-${arch}${musl ? "-musl" : ""}`;
const cliSpecifier = `${cliPackage}/claude${exe}`;
const outfile = resolve(values.outfile ?? join(repoRoot, "dist-bin", `claude-agent-acp${exe}`));
// Resolve from the SDK, the way claudeCliPath() does at runtime, so a nested
// install is checked rather than only a hoisted one.
const sdkRequire = createRequire(
createRequire(join(repoRoot, "package.json")).resolve("@anthropic-ai/claude-agent-sdk"),
);
let cliPath;
try {
cliPath = sdkRequire.resolve(cliSpecifier);
} catch {
const sdkVersion = JSON.parse(
readFileSync(
join(dirname(sdkRequire.resolve("@anthropic-ai/claude-agent-sdk")), "package.json"),
"utf8",
),
).version;
console.error(
`${cliSpecifier} is not installed. npm only installs the host's platform package; for ${target} run:\n\n` +
` npm install --no-save --force ${cliPackage}@${sdkVersion}\n`,
);
process.exit(1);
}
const bun = spawnSync(
"bun",
["build", "src/binary-entry.ts", "--compile", "--target", target, "--outfile", outfile],
{ cwd: repoRoot, stdio: "inherit", shell: process.platform === "win32" },
);
if (bun.error) {
console.error(`Could not run bun: ${bun.error.message}. Install it from https://bun.sh.`);
process.exit(1);
}
if (bun.status !== 0) process.exit(bun.status ?? 1);
// The executable runs the CLI from its own directory (src/binary-entry.ts).
const cliOutfile = join(dirname(outfile), `claude${exe}`);
copyFileSync(cliPath, cliOutfile);
chmodSync(cliOutfile, 0o755);
console.log(`Copied ${cliSpecifier} to ${cliOutfile}`);

470
scripts/smoke-binary.mjs Normal file
View File

@@ -0,0 +1,470 @@
#!/usr/bin/env node
// Smoke-tests a built agent over ACP. Defaults to the executable from
// scripts/build-binary.mjs, with `claude` next to it. Pass another command
// after `--` to compare, e.g.
// `node scripts/smoke-binary.mjs --full -- node "$PWD/dist/index.js"` (the full
// run starts the agent in a scratch directory, so paths must be absolute).
//
// node scripts/smoke-binary.mjs # no login needed
// node scripts/smoke-binary.mjs --full # talks to the model; needs a logged-in Claude
//
// SMOKE_DEBUG=1 prints every tool call the full run saw.
//
// The basic run checks `--version`, `--cli --version`, `initialize` (v1 and
// v2) and the `terminal-auth` login command. The
// full run also has Claude use Bash, Read (text and image), Edit and a
// subagent in a scratch directory, then cancels a turn, lists the session,
// loads it in a fresh agent process, and forks and deletes it.
//
// Claude Code's native builds have no Grep or Glob tool: its Bash shell wraps
// `grep` and `find` in functions that run the ugrep and bfs built into the
// Claude binary at `$CLAUDE_CODE_EXECPATH`. The full run checks that those
// run, since they are the helpers a packaged build could fail to reach.
import { spawn, spawnSync } from "node:child_process";
import {
copyFileSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import readline from "node:readline";
import { fileURLToPath } from "node:url";
import { parseArgs } from "node:util";
import { deflateSync } from "node:zlib";
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const { values, positionals } = parseArgs({
options: { full: { type: "boolean", default: false } },
allowPositionals: true,
});
const exe = process.platform === "win32" ? ".exe" : "";
const [command, ...commandArgs] =
positionals.length > 0 ? positionals : [join(repoRoot, "dist-bin", `claude-agent-acp${exe}`)];
let failures = 0;
function check(ok, label, detail) {
console.log(
`${ok ? "ok " : "FAIL"} ${label}${!ok && detail !== undefined ? `: ${detail}` : ""}`,
);
if (!ok) failures++;
return ok;
}
// ---------------------------------------------------------------- basic
function runSync(args, [cmd, ...cmdArgs] = [command, ...commandArgs]) {
const result = spawnSync(cmd, [...cmdArgs, ...args], {
encoding: "utf8",
timeout: 60_000,
});
return { stdout: (result.stdout ?? "").trim(), error: result.error ?? result.stderr };
}
const version = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")).version;
const own = runSync(["--version"]);
check(own.stdout === version, `--version prints ${version}`, own.stdout || own.error);
const cli = runSync(["--cli", "--version"]);
check(
/\(Claude Code\)$/.test(cli.stdout),
"--cli --version runs the bundled Claude CLI",
cli.stdout || cli.error,
);
// The release archive's layout: the executable finds `claude` in the directory
// of its real path. Only for the default executable; a `--` command has no
// such layout.
if (positionals.length === 0) {
const scratch = mkdtempSync(join(tmpdir(), "acp-layout-"));
try {
if (process.platform !== "win32") {
const link = join(scratch, "claude-agent-acp");
symlinkSync(command, link);
const linked = runSync(["--cli", "--version"], [link]);
check(
/\(Claude Code\)$/.test(linked.stdout),
"a symlink to the executable finds the bundled CLI",
linked.stdout || linked.error,
);
rmSync(link);
}
const alone = join(scratch, `claude-agent-acp${exe}`);
copyFileSync(command, alone);
const missing = runSync(["--cli", "--version"], [alone]);
check(
String(missing.error).includes("The Claude CLI is missing"),
"without claude next to it, says the CLI is missing",
missing.stdout || missing.error,
);
} finally {
rmSync(scratch, { recursive: true, force: true });
}
}
/** Starts the agent and speaks newline-delimited JSON-RPC to it. */
function startAgent(cwd, onNotification) {
const child = spawn(command, commandArgs, { cwd, stdio: ["pipe", "pipe", "pipe"] });
const stderr = [];
child.stderr.on("data", (chunk) => stderr.push(chunk));
let nextId = 0;
const pending = new Map();
const write = (message) =>
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", ...message }) + "\n");
readline.createInterface({ input: child.stdout }).on("line", (line) => {
let message;
try {
message = JSON.parse(line);
} catch {
check(false, "stdout carries only JSON-RPC", line.slice(0, 200));
return;
}
if (message.method === undefined && pending.has(message.id)) {
pending.get(message.id)(message);
pending.delete(message.id);
} else if (message.method === "session/request_permission") {
const allow =
message.params.options.find((o) => o.kind === "allow_once") ?? message.params.options[0];
write({
id: message.id,
result: { outcome: { outcome: "selected", optionId: allow.optionId } },
});
} else if (message.id !== undefined && message.method !== undefined) {
write({
id: message.id,
error: { code: -32601, message: `smoke client does not handle ${message.method}` },
});
} else {
onNotification?.(message);
}
});
const exited = new Promise((resolve) => child.on("exit", resolve));
// Answer anything still pending if the agent dies, instead of hanging.
let exitError;
exited.then((code) => {
exitError = { message: `agent exited with ${code}: ${Buffer.concat(stderr)}` };
for (const resolve of pending.values()) resolve({ error: exitError });
pending.clear();
});
return {
request(method, params) {
const id = ++nextId;
return new Promise((resolve) => {
if (exitError) return resolve({ error: exitError });
pending.set(id, resolve);
write({ id, method, params });
});
},
notify: (method, params) => write({ method, params }),
async stop() {
child.stdin.end();
const code = await Promise.race([
exited,
new Promise((r) => setTimeout(() => r("timeout"), 10_000)),
]);
if (code === "timeout") child.kill();
return { code, stderr: Buffer.concat(stderr).toString() };
},
};
}
const initializeParams = { protocolVersion: 1, clientCapabilities: {} };
{
const agent = startAgent(process.cwd());
const init = await agent.request("initialize", initializeParams);
check(
init.result?.agentInfo?.version === version,
"initialize answers",
JSON.stringify(init.error),
);
const { code } = await agent.stop();
check(code === 0, "exits 0 when stdin closes", code);
}
{
// The `terminal-auth` login command reruns the agent; run it with
// `--version` in place of `auth login --console` instead of logging in.
// `--version` wins over a stray argument, so also check that no path inside
// a compiled executable leaked into the arguments.
const agent = startAgent(process.cwd());
const init = await agent.request("initialize", {
protocolVersion: 1,
clientCapabilities: { _meta: { "terminal-auth": true } },
});
await agent.stop();
const login = init.result?.authMethods?.find((m) => m.id === "console-login")?._meta?.[
"terminal-auth"
];
const cliArgs = login?.args?.slice(0, login.args.indexOf("--cli") + 1);
const rerun = login
? runSync([...cliArgs, "--version"], [login.command])
: { stdout: "", error: "no terminal-auth console-login method" };
check(
/\(Claude Code\)$/.test(rerun.stdout) &&
!login.args.some((arg) => arg.includes("$bunfs") || arg.includes("~BUN")),
"the terminal-auth login command reruns the agent",
`${JSON.stringify(login)} -> ${rerun.stdout || rerun.error}`,
);
}
{
const agent = startAgent(process.cwd());
const init = await agent.request("initialize", {
protocolVersion: 2,
info: { name: "smoke-binary", version },
capabilities: {},
});
check(
init.result?.protocolVersion === 2,
"initialize answers ACP v2",
JSON.stringify(init.error),
);
await agent.stop();
}
// ---------------------------------------------------------------- full
if (values.full) {
const work = mkdtempSync(join(tmpdir(), "acp-smoke-"));
try {
await fullSmoke(work);
} finally {
rmSync(work, { recursive: true, force: true });
}
}
/** A solid red 16x16 PNG, so the model has something to name. */
function redPng() {
const crcTable = Array.from({ length: 256 }, (_, n) => {
let c = n;
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
return c >>> 0;
});
const crc = (buf) => {
let c = 0xffffffff;
for (const byte of buf) c = crcTable[(c ^ byte) & 0xff] ^ (c >>> 8);
return (c ^ 0xffffffff) >>> 0;
};
const chunk = (type, data) => {
const body = Buffer.concat([Buffer.from(type), data]);
const out = Buffer.alloc(body.length + 8);
out.writeUInt32BE(data.length, 0);
body.copy(out, 4);
out.writeUInt32BE(crc(body), body.length + 4);
return out;
};
const size = 16;
const header = Buffer.alloc(13);
header.writeUInt32BE(size, 0);
header.writeUInt32BE(size, 4);
header[8] = 8; // bit depth
header[9] = 2; // RGB
const row = Buffer.concat([Buffer.from([0]), Buffer.alloc(size * 3, Buffer.from([255, 0, 0]))]);
return Buffer.concat([
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
chunk("IHDR", header),
chunk("IDAT", deflateSync(Buffer.concat(Array(size).fill(row)))),
chunk("IEND", Buffer.alloc(0)),
]);
}
/** Writes its results to files so the checks don't depend on the model's reply. */
function searchCommand(token) {
return [
`grep -rl "${token}" . > grep.out`,
'find . -name "*.md" > find.out',
'{ echo "$CLAUDE_CODE_EXECPATH"; grep --version | head -1; find --version | head -1; } > tools.out',
].join("; ");
}
function searchResults(work) {
const read = (name) => {
try {
return readFileSync(join(work, name), "utf8");
} catch (error) {
return String(error);
}
};
const [execPath = "", grepVersion = "", findVersion = ""] = read("tools.out").split("\n");
return { grep: read("grep.out"), find: read("find.out"), execPath, grepVersion, findVersion };
}
async function fullSmoke(work) {
const token = `SMOKE_TOKEN_${process.pid}`;
mkdirSync(join(work, "notes"));
mkdirSync(join(work, "src"));
writeFileSync(join(work, "notes", "alpha.md"), `# Alpha\n\nThe token is ${token}.\n`);
writeFileSync(join(work, "notes", "beta.md"), "# Beta\n\nNothing here.\n");
writeFileSync(join(work, "src", "gamma.ts"), `export const value = "${token}";\n`);
writeFileSync(join(work, "pixel.png"), redPng());
const toolCalls = new Map();
let text = "";
const onNotification = (message) => {
if (message.method !== "session/update") return;
const update = message.params.update;
if (update.sessionUpdate === "agent_message_chunk" && update.content.type === "text") {
text += update.content.text;
} else if (
update.sessionUpdate === "tool_call" ||
update.sessionUpdate === "tool_call_update"
) {
const call = toolCalls.get(update.toolCallId) ?? {};
toolCalls.set(update.toolCallId, {
...call,
name: update._meta?.claudeCode?.toolName ?? call.name,
status: update.status ?? call.status,
title: update.title ?? call.title,
});
}
};
const agent = startAgent(work, onNotification);
await agent.request("initialize", initializeParams);
const created = await agent.request("session/new", { cwd: work, mcpServers: [] });
if (!check(created.result?.sessionId, "session/new", JSON.stringify(created.error))) {
console.error((await agent.stop()).stderr);
return;
}
const sessionId = created.result.sessionId;
const started = Date.now();
const prompt = await agent.request("session/prompt", {
sessionId,
prompt: [
{
type: "text",
text: [
"This is an automated smoke test of your tools. Do each step with the named tool, in order, one tool call per step:",
`1. Bash: run exactly \`${searchCommand(token)}\``,
"2. Read: read notes/alpha.md.",
"3. Read: read pixel.png and note its color.",
`4. Edit: in notes/alpha.md replace "${token}" with "EDITED".`,
"5. Bash: run `cat notes/alpha.md`.",
'6. Agent: launch a subagent (subagent_type "Explore") that counts the .ts files under src/ and reports the number.',
"Then reply with one line: SMOKE-DONE color=<the color of pixel.png> tsfiles=<the subagent's count>.",
].join("\n"),
},
],
});
check(
prompt.result?.stopReason === "end_turn",
"prompt ends with end_turn",
JSON.stringify(prompt.result ?? prompt.error),
);
console.log(` (turn took ${Math.round((Date.now() - started) / 1000)}s)`);
const calls = [...toolCalls.values()];
if (process.env.SMOKE_DEBUG) console.log(JSON.stringify(calls, null, 1));
const searched = searchResults(work);
check(
searched.grep.includes("alpha.md") && searched.grep.includes("gamma.ts"),
"grep found the token",
searched.grep,
);
check(
searched.find.includes("alpha.md") && searched.find.includes("beta.md"),
"find listed the .md files",
searched.find,
);
check(
/^ugrep /.test(searched.grepVersion),
"grep ran Claude's embedded ugrep",
searched.grepVersion,
);
check(/^bfs /.test(searched.findVersion), "find ran Claude's embedded bfs", searched.findVersion);
console.log(` (CLAUDE_CODE_EXECPATH=${searched.execPath})`);
for (const name of ["Read", "Edit", "Bash"]) {
const used = calls.filter((c) => c.name === name);
check(
used.length > 0 && used.every((c) => c.status === "completed"),
`${name} ran and completed`,
JSON.stringify(used),
);
}
const subagent = calls.filter((c) => c.name === "Agent" || c.name === "Task");
check(
subagent.some((c) => c.status === "completed"),
"subagent ran and completed",
JSON.stringify(subagent),
);
const failed = calls.filter((c) => c.status === "failed");
check(failed.length === 0, "no tool call failed", JSON.stringify(failed));
check(
readFileSync(join(work, "notes", "alpha.md"), "utf8").includes("EDITED"),
"Edit changed the file on disk",
);
check(
/SMOKE-DONE/.test(text) && /color=\W*red/i.test(text),
"image read as red",
text.slice(-300),
);
check(/tsfiles=\W*1\b/.test(text), "subagent counted 1 .ts file", text.slice(-300));
// A cancel interrupts a running Bash command and ends the turn as cancelled.
const cancelled = agent.request("session/prompt", {
sessionId,
prompt: [
{ type: "text", text: "Use the Bash tool to run `sleep 60`. Do not run anything else." },
],
});
const deadline = Date.now() + 60_000;
const bashRunning = () =>
[...toolCalls.values()].some((c) => c.name === "Bash" && c.status !== "completed");
while (!bashRunning() && Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 200));
}
agent.notify("session/cancel", { sessionId });
const cancelResult = await cancelled;
check(
cancelResult.result?.stopReason === "cancelled",
"session/cancel ends the turn as cancelled",
JSON.stringify(cancelResult.result ?? cancelResult.error),
);
const listed = await agent.request("session/list", { cwd: work });
check(
listed.result?.sessions?.some((s) => s.sessionId === sessionId),
"session/list finds the session",
JSON.stringify(listed.result ?? listed.error),
);
const { code, stderr } = await agent.stop();
check(code === 0, "exits 0 when stdin closes", code);
if (failures > 0) console.error(stderr);
// A fresh process, so the replay comes from the transcript on disk.
let replayed = "";
const reloaded = startAgent(work, (message) => {
const update = message.params?.update;
if (update?.sessionUpdate === "user_message_chunk" && update.content.type === "text")
replayed += update.content.text;
});
await reloaded.request("initialize", initializeParams);
const loaded = await reloaded.request("session/load", { sessionId, cwd: work, mcpServers: [] });
check(!loaded.error, "session/load in a new process", JSON.stringify(loaded.error));
check(replayed.includes(token), "session/load replays the first prompt", replayed.slice(0, 200));
// Fork and delete go through the SDK's transcript helpers, not the CLI.
const forked = await reloaded.request("session/fork", { sessionId, cwd: work, mcpServers: [] });
const forkId = forked.result?.sessionId;
check(
forkId && forkId !== sessionId,
"session/fork",
JSON.stringify(forked.result ?? forked.error),
);
if (forkId) {
const deleted = await reloaded.request("session/delete", { sessionId: forkId });
const after = await reloaded.request("session/list", { cwd: work });
check(
!deleted.error && !after.result?.sessions?.some((s) => s.sessionId === forkId),
"session/delete removes the fork",
JSON.stringify(deleted.error ?? after.result),
);
}
await reloaded.stop();
}
console.log(failures === 0 ? "\nsmoke test passed" : `\n${failures} check(s) failed`);
process.exit(failures === 0 ? 0 : 1);

View File

@@ -1,13 +1,11 @@
import {
agent as acpAgent,
AgentApp,
AgentContext,
AuthenticateRequest,
AuthMethod,
AvailableCommand,
CancelNotification,
ClientCapabilities,
CompleteElicitationNotification,
CreateElicitationRequest,
CreateElicitationResponse,
DisableProviderRequest,
@@ -37,7 +35,6 @@ import {
RequestError,
SetProviderRequest,
SetProviderResponse,
RequestPermissionRequest,
RequestPermissionResponse,
ResumeSessionRequest,
ResumeSessionResponse,
@@ -106,6 +103,10 @@ import {
toGoalSnapshot,
} from "./goal-extension.js";
import { blobResourceToClaude } from "./blob-resources.js";
import { ChangedMetaClient, ClientConnection } from "./acp-client.js";
import type { AcpClient, Logger } from "./acp-client.js";
export type { AcpClient, Logger } from "./acp-client.js";
import { promptSuggestionsMeta, promptSuggestionText } from "./prompt-suggestions.js";
import { sanitizeTitle, SessionTitles } from "./session-titles.js";
import {
@@ -165,20 +166,11 @@ import {
clientSupportsAsyncTasks,
} from "./async-tasks.js";
import {
backgroundLevel,
type BackgroundLevel,
type BackgroundSink,
backgroundToolCallPause,
backgroundToolCallStates,
type BackgroundTaskEnd,
type BackgroundToolCall,
BackgroundWorkReporter,
cancelBackgroundWork,
droppedTasks,
endBackgroundWork,
registerBackgroundToolCalls,
takeBackgroundTerminalExit,
takeBackgroundToolCallEnd,
SessionBackgroundWork,
} from "./background-work.js";
import {
AUTH_STATUS_PROBE_TIMEOUT_MS,
@@ -193,6 +185,8 @@ import {
notLoggedInAuthStatus,
sameAuthStatus,
} from "./auth-status.js";
import { agentCommand } from "./agent-command.js";
import { claudeCliPath } from "./claude-cli.js";
import { ContentBlockParam } from "@anthropic-ai/sdk/resources";
import { BetaContentBlock, BetaRawContentBlockDelta } from "@anthropic-ai/sdk/resources/beta.mjs";
import { execFile } from "node:child_process";
@@ -239,7 +233,7 @@ import {
type ResumedSessionSnapshot,
} from "./resumed-session.js";
import { SessionTiming } from "./session-timing.js";
import { ALLOW_BYPASS } from "./permissions/modes.js";
import { ALLOW_BYPASS, autoModeDisabled } from "./permissions/modes.js";
import { normalizeDurablePermissionChangeSet } from "./permissions/normalization.js";
import { buildClaudePermissionOptions } from "./permissions/options.js";
import {
@@ -288,8 +282,10 @@ import {
contextCompactionMetadataFromBoundary,
isCompactSummaryMessage,
} from "./context-compaction.js";
import { IdleCompaction } from "./idle-compaction.js";
import {
clientSupportsNotices,
collapseHookSpeaker,
MAX_NOTICE_TITLE_LENGTH,
normalizeNoticeText,
noticeOrTranscriptUpdate,
@@ -323,7 +319,6 @@ import {
withRunningTerminal,
withRunningToolCall,
} from "./tool-calls/background.js";
import { ChangedMetaFilter } from "./tool-calls/changed-meta-filter.js";
import { ToolCallFieldTracker } from "./tool-calls/field-tracker.js";
import { ClientCapabilities as ToolCallClientCapabilities } from "./tool-calls/client-capabilities.js";
import {
@@ -432,16 +427,6 @@ function isEmptyUserInterruptionDiagnostic(
);
}
/**
* Logger interface for customizing logging output
*/
export interface Logger {
log: (...args: any[]) => void;
error: (...args: any[]) => void;
/** Optional: a caller that supplies no `warn` gets its warnings on `error`. */
warn?: (...args: any[]) => void;
}
type AccumulatedUsage = {
inputTokens: number;
outputTokens: number;
@@ -804,6 +789,11 @@ function parseSteerRequest(params: unknown): SteerRequest {
};
}
/** Who started a turn. `idleCompaction` is the adapter's own `/compact` (see
* `IdleCompaction`): it is not a user prompt, so it neither disarms idle
* compaction, asks for a title from its prompt, nor clears the published prompt suggestion. */
type TurnOrigin = "client" | "idleCompaction";
/** One in-flight turn, started by `startTurn()`. A persistent per-session
* consumer (see `runConsumer`) drains the SDK query stream for the whole
* session and settles each Turn when that turn's outcome is known, so
@@ -1476,30 +1466,17 @@ export type Session = {
* to stop each one itself. A client that can cancel one subagent gets the
* declaration, but it has no control for a workflow unless it is AIR with
* `asyncTasks` (`_session/async_task/stop`), or gets the session's
* background work (`reportsBackground`, with `tool_call/cancel`), which
* background work (`background.reported`, with `tool_call/cancel`), which
* `session/cancel` must leave running. So for any other such client the
* adapter stops the workflows after the interrupt, as the interrupt did
* before. */
stopsWorkflowsOnCancel?: boolean;
/** The display terminals of the commands that went to the background and
* still run, by the id of the command's task: the Bash tool call whose
* terminal it is, and the subagent tool call that ran it, if any. The
* terminal exits when the task ends, not at the tool result (see
* `withRunningTerminal`). Only for a client that renders terminals and is
* not AIR, which shows the command through its async tasks. */
backgroundTerminals?: Map<string, { toolCallId: string; parentToolUseId?: string }>;
/** The tool calls whose tasks went on in the background and still run, by
* task id: a Monitor, a Workflow, or a backgrounded MCP call. The tool call
* stays `in_progress` (or `paused`) after its result, until its task ends
* (see `withRunningToolCall`). Only while `reportsBackground`. */
backgroundToolCalls?: Map<string, BackgroundToolCall>;
/** Whether the client gets the session's background work (see
* `background-work.ts`). Only when Claude Code's interrupt spares
* background tasks, as `perTaskStopAffordance` makes it. */
reportsBackground?: boolean;
/** Claude Code's live background task set, from its last
* `background_tasks_changed`. Per CLI process, as is a session. */
backgroundLevel?: BackgroundLevel;
/** The tasks that Claude Code runs in the background for the session: the
* terminals of commands, kept for a client that renders terminals and is
* not AIR (AIR shows a command through its async tasks), and, when the
* client gets the session's background work, the tool calls of tasks and
* the `background` lists. Per CLI process, as is a session. */
background: SessionBackgroundWork;
/** The parent tool call of each subagent that a settle bookend removed from
* `liveBackgroundTasks`. The SDK can resume such a subagent under the same
* agent id without a new `task_started` (a running `task_updated` patch or
@@ -1540,6 +1517,8 @@ export type Session = {
/** The consumer's compaction lifecycle, exposed so the PostCompact hook can
* hand it the retained summary. */
contextCompaction?: ContextCompactionLifecycle;
/** Compacts the session shortly before its one-hour prompt cache expires. */
idleCompaction?: IdleCompaction;
/** Whether any top-level assistant text reached the client since the last
* stretch boundary. Set as a side effect of sending in the consumer's
* `sendUpdate`, never at an emission site; read at the terminal `result`
@@ -2184,54 +2163,6 @@ function recoveredToolInput(prefix: string): Record<string, unknown> | undefined
}
}
export async function claudeCliPath(): Promise<string> {
if (process.env.CLAUDE_CODE_EXECUTABLE) {
return process.env.CLAUDE_CODE_EXECUTABLE;
}
// The SDK's CLI is a native binary shipped as a platform-specific optional
// dependency of @anthropic-ai/claude-agent-sdk. Resolve via a require bound
// to the SDK so nested installs are found even when npm doesn't hoist.
const { createRequire } = await import("node:module");
const req = createRequire(import.meta.resolve("@anthropic-ai/claude-agent-sdk"));
const ext = process.platform === "win32" ? ".exe" : "";
// On linux, both glibc and musl variants may be installed side-by-side
// (e.g. bunx hydrates every optional dep), so picking one by trial is
// unreliable: the wrong binary segfaults at runtime instead of failing to
// spawn. Detect the runtime libc and prefer the matching variant, falling
// back to the other only if the preferred one isn't installed.
const candidates =
process.platform === "linux"
? isMuslLibc()
? [
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}-musl/claude${ext}`,
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}/claude${ext}`,
]
: [
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}/claude${ext}`,
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}-musl/claude${ext}`,
]
: [`@anthropic-ai/claude-agent-sdk-${process.platform}-${process.arch}/claude${ext}`];
for (const candidate of candidates) {
try {
return req.resolve(candidate);
} catch {
// try next candidate
}
}
throw new Error(
`Claude native binary not found for ${process.platform}-${process.arch}. ` +
`Reinstall @anthropic-ai/claude-agent-sdk without --omit=optional, or set CLAUDE_CODE_EXECUTABLE.`,
);
}
function isMuslLibc(): boolean {
// process.report.getReport().header.glibcVersionRuntime is populated when
// Node is dynamically linked against glibc, and absent on musl.
const report = process.report?.getReport() as
{ header?: { glibcVersionRuntime?: string } } | undefined;
return !report?.header?.glibcVersionRuntime;
}
/** Returned to clients when a prompt or cancel targets a session whose SDK
* query stream has already ended (ran to `done` or died). The stream is not
* revivable, so the only recovery is a fresh session. */
@@ -2468,137 +2399,6 @@ export function isReplayHiddenMetaMessage(message: unknown): boolean {
);
}
/**
* Client-facing surface the agent calls back into. This is the subset of ACP
* client methods the agent actually uses, expressed as a narrow interface so
* tests can supply lightweight mocks. In production it is backed by
* {@link ClientConnection} over the SDK's typed `AgentContext`.
*/
export interface AcpClient {
sessionUpdate(params: AcpSessionNotification): Promise<void>;
/** `signal`, when aborted, sends `$/cancel_request` for the in-flight
* permission request so the client can dismiss its prompt, and rejects the
* call at once: a client that ignores the cancel never answers. */
requestPermission(
params: AcpPermissionRequest,
signal?: AbortSignal,
): Promise<RequestPermissionResponse>;
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse>;
writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse>;
/** `signal`, when aborted, sends `$/cancel_request` for the in-flight
* elicitation so the client can dismiss its prompt, and rejects the call
* at once, as {@link requestPermission} does. */
createElicitation(
params: CreateElicitationRequest,
signal?: AbortSignal,
): Promise<CreateElicitationResponse>;
completeElicitation(params: CompleteElicitationNotification): Promise<void>;
/** Send a custom (extension) notification, e.g. `_claude/sdkMessage`. */
extNotification(method: string, params: Record<string, unknown>): Promise<void>;
}
/**
* Bridges {@link AcpClient} to the connection-scoped {@link AgentContext}
* exposed by `AgentApp.connect(...)` as `connection.client`. The peer handle is
* valid for the entire connection lifetime, so it is captured once at
* construction.
*/
class ClientConnection implements AcpClient {
constructor(private readonly ctx: AgentContext) {}
sessionUpdate(params: AcpSessionNotification): Promise<void> {
return this.ctx.notify(methods.client.session.update, asSdkSessionNotification(params));
}
requestPermission(
params: RequestPermissionRequest,
signal?: AbortSignal,
): Promise<RequestPermissionResponse> {
return this.ctx.request(methods.client.session.requestPermission, params, {
cancellationSignal: signal,
stopWaitingOnCancel: true,
});
}
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
return this.ctx.request(methods.client.fs.readTextFile, params);
}
writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse> {
return this.ctx.request(methods.client.fs.writeTextFile, params);
}
createElicitation(
params: CreateElicitationRequest,
signal?: AbortSignal,
): Promise<CreateElicitationResponse> {
return this.ctx.request(methods.client.elicitation.create, params, {
cancellationSignal: signal,
stopWaitingOnCancel: true,
});
}
completeElicitation(params: CompleteElicitationNotification): Promise<void> {
return this.ctx.notify(methods.client.elicitation.complete, params);
}
extNotification(method: string, params: Record<string, unknown>): Promise<void> {
return this.ctx.notify(method, params);
}
}
/**
* The client that the agent talks to. For an AIR client, every session update
* passes the {@link ChangedMetaFilter} last, after the native subagent routing,
* so a `tool_call_update` carries only the `_meta` keys that changed. ACP
* merges only the top-level tool call fields, not the `_meta` keys. So another
* client gets the full `_meta` on each update.
*/
class ChangedMetaClient implements AcpClient {
private readonly filter = new ChangedMetaFilter();
constructor(
private readonly inner: AcpClient,
private readonly airClient: () => boolean,
) {}
async sessionUpdate(params: AcpSessionNotification): Promise<void> {
if (!this.airClient()) return this.inner.sessionUpdate(params);
const update = this.filter.apply(params.update as SessionNotification["update"]);
if (update) await this.inner.sessionUpdate({ ...params, update } as AcpSessionNotification);
}
requestPermission(
params: AcpPermissionRequest,
signal?: AbortSignal,
): Promise<RequestPermissionResponse> {
return this.inner.requestPermission(params, signal);
}
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
return this.inner.readTextFile(params);
}
writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse> {
return this.inner.writeTextFile(params);
}
createElicitation(
params: CreateElicitationRequest,
signal?: AbortSignal,
): Promise<CreateElicitationResponse> {
return this.inner.createElicitation(params, signal);
}
completeElicitation(params: CompleteElicitationNotification): Promise<void> {
return this.inner.completeElicitation(params);
}
extNotification(method: string, params: Record<string, unknown>): Promise<void> {
return this.inner.extNotification(method, params);
}
}
function raceWithAbort<T>(operation: Promise<T>, signal: AbortSignal): Promise<T> {
return new Promise<T>((resolve, reject) => {
const cleanup = () => signal.removeEventListener("abort", onAbort);
@@ -3050,10 +2850,11 @@ export class ClaudeAcpAgent {
};
if (supportsMetaTerminalAuth) {
const agent = agentCommand();
remoteLoginMethod._meta = {
"terminal-auth": {
command: process.execPath,
args: [...process.argv.slice(1), "--cli"],
command: agent.command,
args: [...agent.args, "--cli"],
label: "Claude Login",
},
};
@@ -3080,18 +2881,18 @@ export class ClaudeAcpAgent {
};
if (supportsMetaTerminalAuth) {
const baseArgs = process.argv.slice(1);
const agent = agentCommand();
claudeLoginMethod._meta = {
"terminal-auth": {
command: process.execPath,
args: [...baseArgs, "--cli", "auth", "login", "--claudeai"],
command: agent.command,
args: [...agent.args, "--cli", "auth", "login", "--claudeai"],
label: "Claude Login",
},
};
consoleLoginMethod._meta = {
"terminal-auth": {
command: process.execPath,
args: [...baseArgs, "--cli", "auth", "login", "--console"],
command: agent.command,
args: [...agent.args, "--cli", "auth", "login", "--console"],
label: "Anthropic Console Login",
},
};
@@ -3949,7 +3750,7 @@ export class ClaudeAcpAgent {
* `promptToClaude` would drop it. v2 prompts reach `startTurn` through
* `v1PromptRequest`, which rejects these blocks itself.
*/
async prompt(params: PromptRequest): Promise<PromptResponse> {
async prompt(params: PromptRequest, origin: TurnOrigin = "client"): Promise<PromptResponse> {
for (const block of params.prompt) {
if (!SUPPORTED_PROMPT_CONTENT_TYPES.includes(block.type)) {
throw RequestError.invalidParams(
@@ -3962,7 +3763,7 @@ export class ClaudeAcpAgent {
const outcome = new Promise<TurnOutcome>((resolve, reject) => {
events = { inserted() {}, ended: resolve, failed: reject };
});
await this.startTurn(params, events);
await this.startTurn(params, events, origin);
return outcome;
}
@@ -3972,7 +3773,15 @@ export class ClaudeAcpAgent {
* on, `events` reports how the turn goes (see {@link TurnEvents}), possibly
* before this resolves.
*/
async startTurn(params: PromptRequest, events: TurnEvents): Promise<void> {
async startTurn(
params: PromptRequest,
events: TurnEvents,
origin: TurnOrigin = "client",
): Promise<void> {
// Before the first await below: a timer that fires during those awaits
// would queue `/compact` right behind this prompt. The idle compaction's
// own turn comes from that timer, which is already cleared.
if (origin === "client") this.sessions[params.sessionId]?.idleCompaction?.disarm("prompt");
const received = ++this.promptsReceived;
while (this.providerUpdate) await this.providerUpdate;
let session = this.sessions[params.sessionId];
@@ -4024,10 +3833,7 @@ export class ClaudeAcpAgent {
// around it. Refuse instead of interleaving. `foregroundWork` also covers
// a cycle that Claude Code started on its own, such as its follow-up
// after a background subagent finished, which is not in `turnQueue`.
if (
this.foregroundWork.has(params.sessionId) ||
(session.turnQueue ?? []).some((turn) => !turn.settled)
) {
if (this.claudeIsWorking(params.sessionId, session)) {
throw RequestError.invalidRequest(
undefined,
"Remote Control can't be changed while Claude is working. Try again when the turn ends.",
@@ -4091,18 +3897,22 @@ export class ClaudeAcpAgent {
const localCommand = this.localCommandMarkdown(params);
const localCommandAbort = localCommand ? new AbortController() : undefined;
session.titles.onPrompt(params.prompt);
// An idle compaction is not something the user sent: it must not title the
// session `/compact` or spend the prediction the user is still looking at.
if (origin === "client") {
session.titles.onPrompt(params.prompt);
// The prompt the previous turn predicted is spent: the user either sent it
// or sent something else. Clearing it here keeps a turn that predicts
// nothing (the CLI predicts none in plan mode, after an API error, or at
// the usage limit) from leaving the stale prediction in the composer. A
// session that holds no prediction needs no clear, and the prompt path
// then keeps the ordering it has without this feature.
if (session.lastPublishedPromptSuggestion != null) {
await this.publishPromptSuggestion(params.sessionId, null);
} else {
session.promptSuggestionExpected = false;
// The prompt the previous turn predicted is spent: the user either sent it
// or sent something else. Clearing it here keeps a turn that predicts
// nothing (the CLI predicts none in plan mode, after an API error, or at
// the usage limit) from leaving the stale prediction in the composer. A
// session that holds no prediction needs no clear, and the prompt path
// then keeps the ordering it has without this feature.
if (session.lastPublishedPromptSuggestion != null) {
await this.publishPromptSuggestion(params.sessionId, null);
} else {
session.promptSuggestionExpected = false;
}
}
// A `/remote-control` command still in flight answers into the session's
@@ -4160,7 +3970,7 @@ export class ClaudeAcpAgent {
(session.sentUserUuids ??= new Set()).add(promptUuid);
session.input.push(userMessage);
this.ensureConsumer(session, params.sessionId);
session.titles.onPromptQueued(session, params.prompt);
if (origin === "client") session.titles.onPromptQueued(session, params.prompt);
// The prompt is queued, so its turn goes ahead even if the client misses
// the goal it sets; the turn's events report the outcome.
await this.publishGoalFromPrompt(params.sessionId, firstText, promptUuid).catch((error) =>
@@ -4636,6 +4446,7 @@ export class ClaudeAcpAgent {
// check: the interrupt can have the CLI finalizing the aborted cycle by the
// time the consumer next runs, and an unmarked result would settle the turn
// (see Turn.steeredEchoes).
session.idleCompaction?.disarm("steer");
(turnInFlight.steeredEchoes ??= new Set()).add(steeredUuid);
(turnInFlight.steeredUuids ??= new Set()).add(steeredUuid);
// A turn already held for background subagents has a recorded outcome the
@@ -4709,6 +4520,8 @@ export class ClaudeAcpAgent {
let lastAssistantTotalUsage: number | null = null;
let lastAssistantUsage: UsageSnapshot | null = null;
let lastAssistantModel: string | null = null;
// A rate_limit_event that came before the turn had any usage to report.
let pendingRateLimit: unknown = null;
// When the Claude SDK classifies a turn as failed (e.g. rate limit, auth
// problem, billing), it sets a categorical `error` field on the
// `SDKAssistantMessage` that precedes the final `result` message. We capture
@@ -5016,7 +4829,8 @@ export class ClaudeAcpAgent {
};
// ACP's usage_update has no model field, so the model the usage belongs
// to rides in `_meta` alongside the other `_claude/*` keys.
// to rides in `_meta` alongside the other `_claude/*` keys, as does a
// pending rate limit.
const attachUsageModel = <
T extends {
sessionUpdate: "usage_update";
@@ -5027,6 +4841,13 @@ export class ClaudeAcpAgent {
>(
update: T,
): T => {
if (pendingRateLimit !== null) {
update = {
...update,
_meta: { "_claude/rateLimit": pendingRateLimit, ...(update._meta ?? {}) },
};
pendingRateLimit = null;
}
if (!lastAssistantModel) return update;
return {
...update,
@@ -5417,16 +5238,10 @@ export class ClaudeAcpAgent {
this.syncAwaitingUser(params.sessionId, session);
};
/** Ends the display terminal of a command that went to the background, and
* gives a tool call whose task went on in the background its final
* status, once the task ends (see `Session.backgroundTerminals` and
* `Session.backgroundToolCalls`). */
const endBackgroundTask = async (taskId: string, end: BackgroundTaskEnd) => {
const exit = takeBackgroundTerminalExit(session, params.sessionId, taskId);
if (exit) await sendUpdate(exit);
const toolCallEnd = takeBackgroundToolCallEnd(session, params.sessionId, taskId, end);
if (toolCallEnd) await sendUpdate(toolCallEnd);
};
/** Ends the terminal and the tool call of a task that ran in the
* background, once the task ends (see `Session.background`). */
const endBackgroundTask = (taskId: string, end: BackgroundTaskEnd) =>
session.background.taskEnded(taskId, end, sendUpdate);
/** Settles a task. Its request may still be open, and an agent without a
* record no longer blocks the foreground work, so that is checked again. */
@@ -6376,6 +6191,7 @@ export class ClaudeAcpAgent {
} else if (message.compact_result === "success") {
await compaction.finish(message.uuid, "completed");
} else if (message.compact_result === "failed") {
session.idleCompaction?.noteCompactionFailed(message.compact_error);
await compaction.finish(message.uuid, "failed", {
...(message.compact_error ? { error: message.compact_error } : {}),
});
@@ -6402,7 +6218,11 @@ export class ClaudeAcpAgent {
await compaction.finish(
message.uuid,
"completed",
compactMetadata ? contextCompactionMetadataFromBoundary(compactMetadata) : {},
compactMetadata
? contextCompactionMetadataFromBoundary(compactMetadata, {
idleCompaction: session.idleCompaction?.isCompacting,
})
: {},
true,
);
const usedTokens = compactMetadata?.post_tokens ?? 0;
@@ -6454,8 +6274,6 @@ export class ClaudeAcpAgent {
!firstUnsettledQueuedTurn()
) {
await asyncTasks.autonomousTurnStarted();
} else if (message.state === "idle" && asyncTasks.autonomousTurnOpen) {
await asyncTasks.autonomousTurnEnded("completed");
}
if (
message.state === "running" &&
@@ -6487,6 +6305,9 @@ export class ClaudeAcpAgent {
// here. An owed idle belongs to an earlier result: it can lag
// past the cycle's first output.
if (session.owedTrailingIdles === 0) {
if (asyncTasks.autonomousTurnOpen) {
await asyncTasks.autonomousTurnEnded("completed");
}
await closeCycleToolCalls();
this.endAutonomousCycle(params.sessionId, {
outcome: { stopReason: session.cancelled ? "cancelled" : "end_turn" },
@@ -6620,6 +6441,10 @@ export class ClaudeAcpAgent {
// generatable; see SessionTitles.onTurnEnd.
await session.titles.onTurnEnd(session);
}
// After the settle block above: steered turns, held turns and
// autonomous cycles count as work until it settles them, and no
// further idle arrives to arm the timer.
if (message.state === "idle") session.idleCompaction?.onIdle();
break;
}
case "memory_recall": {
@@ -6664,6 +6489,16 @@ export class ClaudeAcpAgent {
);
break;
}
case "session_writes_lost": {
// A full disk (ENOSPC) or used-up quota (EDQUOT) kept the CLI
// from saving transcript lines or ~/.claude.json. The CLI sends
// it as it exits, after the last result, so no turn is open to
// carry a session/update: log it like `mirror_error`.
this.logger.error(
`Session ${message.session_id}: Claude Code could not save ${message.lost.join(" and ")} (${message.error_code}); resuming may miss part of the conversation`,
);
break;
}
case "permission_denied": {
unregisterHookCallback(message.tool_use_id);
// A tool call was auto-denied (by a rule, the classifier,
@@ -6745,10 +6580,11 @@ export class ClaudeAcpAgent {
}
}
const severity = message.level === "warning" ? "warning" : "info";
const transcriptContent = collapseHookSpeaker(message.content);
const transcriptText =
message.level === "info"
? message.content
: `**${sentenceCase(message.level)}:** ${message.content}`;
? transcriptContent
: `**${sentenceCase(message.level)}:** ${transcriptContent}`;
await sendUpdate({
sessionId: params.sessionId,
update: noticeOrTranscriptUpdate(
@@ -6931,23 +6767,13 @@ export class ClaudeAcpAgent {
}
settleLiveTask(message.task_id);
} else if (message.patch.status === "paused") {
const pause = backgroundToolCallPause(
session,
params.sessionId,
message.task_id,
true,
);
const pause = session.background.taskPaused(message.task_id, true);
if (pause) await sendUpdate(pause);
} else if (
message.patch.status === "running" ||
message.patch.status === "pending"
) {
const resume = backgroundToolCallPause(
session,
params.sessionId,
message.task_id,
false,
);
const resume = session.background.taskPaused(message.task_id, false);
if (resume) await sendUpdate(resume);
// The SDK can resume a finished subagent under the same
// agent id without a new task_started.
@@ -7118,13 +6944,7 @@ export class ClaudeAcpAgent {
break;
case "background_tasks_changed": {
await asyncTasks.backgroundTasksChanged(message.tasks);
if (session.reportsBackground) {
const level = backgroundLevel(message.tasks);
for (const taskId of droppedTasks(session, level)) {
await endBackgroundTask(taskId, undefined);
}
session.backgroundLevel = level;
}
await session.background.levelChanged(message.tasks, sendUpdate);
// A level signal: the full live background-task set on every
// membership change, with REPLACE semantics. Used only to
// reconcile `liveBackgroundTasks` — dropping (or, for
@@ -7984,6 +7804,15 @@ export class ClaudeAcpAgent {
const model = message.event.message.model;
if (model && model !== "<synthetic>") {
lastAssistantModel = model;
// `message_start` arrives with the first streamed token, close to
// when the provider wrote the cache. The `assistant` message
// below covers a response that was not streamed (the CLI's
// non-streaming fallback); its shared id keeps the per-block
// messages from moving the time.
session.idleCompaction?.noteRequest(
message.event.message.usage,
message.event.message.id || undefined,
);
// Only upgrade from the heuristic default — once we have an
// authoritative window (cache-seeded at session creation or
// on a model switch, read from the resumed session on
@@ -8355,6 +8184,10 @@ export class ClaudeAcpAgent {
lastAssistantUsage = snapshotFromUsage(message.message.usage);
lastAssistantTotalUsage = totalTokens(lastAssistantUsage);
session.contextUsedTokens = lastAssistantTotalUsage;
session.idleCompaction?.noteRequest(
message.message.usage,
message.message.id || undefined,
);
}
lastAssistantWasUsageLimit = isSyntheticUsageLimitMessage(message.message);
if (message.error || lastAssistantWasUsageLimit) {
@@ -8527,6 +8360,7 @@ export class ClaudeAcpAgent {
const acceptedPlanToolUseId = observeExitPlanToolResults(message, content, session);
let backgroundedToolCalls: ReadonlySet<string> = new Set();
let runningTerminal: string | undefined;
let runningTerminalTask: string | undefined;
let runningToolCalls: ReadonlySet<string> = new Set();
if (message.type === "user") {
rememberResolvedToolNames(session, content);
@@ -8540,6 +8374,18 @@ export class ClaudeAcpAgent {
content,
toolUseResult,
session.toolUseCache,
(toolUseId) => {
for (const [taskId, task] of session.liveBackgroundTasks) {
if (
task.parentToolUseId === toolUseId &&
task.taskType === "local_bash" &&
task.backgrounded
) {
return taskId;
}
}
return undefined;
},
);
if (backgroundBashTask) await asyncTasks.taskBackgrounded(backgroundBashTask);
// The command's terminal runs until its task ends, unless the
@@ -8553,14 +8399,18 @@ export class ClaudeAcpAgent {
session.liveBackgroundTasks.has(backgroundBashTask.taskId)
) {
runningTerminal = terminalId;
(session.backgroundTerminals ??= new Map()).set(backgroundBashTask.taskId, {
session.background.terminalStarted(backgroundBashTask.taskId, {
toolCallId: terminalId,
...(message.parent_tool_use_id
? { parentToolUseId: message.parent_tool_use_id }
: {}),
...(typeof backgroundBashTask.outputFilePath === "string"
? { outputFilePath: backgroundBashTask.outputFilePath }
: {}),
});
runningTerminalTask = backgroundBashTask.taskId;
}
runningToolCalls = registerBackgroundToolCalls(
runningToolCalls = session.background.toolResults(
session,
content,
toolUseResult,
@@ -8642,6 +8492,18 @@ export class ClaudeAcpAgent {
),
);
}
// After the result's own updates: the command's output replaces
// the placeholder that they gave its terminal.
if (runningTerminalTask) {
session.background.followOutput(runningTerminalTask, (notification) =>
sendUpdate(notification).catch((error) => {
this.logger.error(
`Session ${params.sessionId}: failed to send background command output:`,
error,
);
}),
);
}
break;
}
case "tool_progress": {
@@ -8697,7 +8559,12 @@ export class ClaudeAcpAgent {
break;
}
case "rate_limit_event": {
if (lastAssistantTotalUsage !== null) {
session.idleCompaction?.noteRateLimit(message.rate_limit_info);
// With partial messages on, the SDK sends this before the turn's
// first assistant frame, while there is no usage yet to send it with.
if (lastAssistantTotalUsage === null) {
pendingRateLimit = message.rate_limit_info;
} else {
await sendUpdate({
sessionId: params.sessionId,
update: attachUsageModel({
@@ -8731,6 +8598,8 @@ export class ClaudeAcpAgent {
// The follow-up prompt was predicted from the transcript the reset
// dropped, so it no longer follows from anything.
await this.publishPromptSuggestion(params.sessionId, null);
// The recorded request and TTL describe the dropped conversation.
session.idleCompaction?.reset();
break;
}
case "tool_use_summary":
@@ -8967,6 +8836,7 @@ export class ClaudeAcpAgent {
return;
}
session.cancelled = true;
session.idleCompaction?.disarm("cancel");
// Only prompts received before this cancel are cancelled. A prompt that
// arrives while the awaits below run is queued and runs after the cancel
// (ACP v2: a `session/prompt` received after `session/cancel` is not
@@ -9358,7 +9228,7 @@ export class ClaudeAcpAgent {
// Its list row now shows what the registry and the transcript tell.
this.sessionIndex.onOwnSessionChanged(sessionId);
// Its background tasks end with the query's process.
for (const exit of endBackgroundWork(session, sessionId)) {
for (const exit of session.background.queryClosed()) {
this.client.sessionUpdate(exit).catch((error) => {
this.logger.error(`Session ${sessionId}: failed to end a background terminal:`, error);
});
@@ -9367,6 +9237,7 @@ export class ClaudeAcpAgent {
}
session.consumer = undefined;
session.contextCompaction = undefined;
session.idleCompaction?.dispose();
session.settingsManager.dispose();
session.input.end();
session.query.close();
@@ -9469,7 +9340,7 @@ export class ClaudeAcpAgent {
/** Stops a command running in the background (v2 `terminal/cancel`, see
* {@link cancelBackgroundWork}). */
cancelBackgroundTerminal(params: { sessionId: string; terminalId: string }): Promise<void> {
return cancelBackgroundWork(this.sessions, params.sessionId, {
return cancelBackgroundWork(Object.values(this.sessions), params.sessionId, {
terminalId: params.terminalId,
});
}
@@ -9477,7 +9348,7 @@ export class ClaudeAcpAgent {
/** Stops the task of a tool call running in the background (v2
* `tool_call/cancel`, see {@link cancelBackgroundWork}). */
cancelBackgroundToolCall(params: { sessionId: string; toolCallId: string }): Promise<void> {
return cancelBackgroundWork(this.sessions, params.sessionId, {
return cancelBackgroundWork(Object.values(this.sessions), params.sessionId, {
toolCallId: params.toolCallId,
});
}
@@ -9492,7 +9363,7 @@ export class ClaudeAcpAgent {
if (!session) return;
const renderer = new AcpToolCallRenderer(this.toolCallCapabilities);
try {
for (const update of backgroundToolCallStates(session, sessionId, toolCallIds, renderer)) {
for (const update of session.background.toolCallStates(toolCallIds, renderer, session.cwd)) {
await this.client.sessionUpdate(update);
}
} catch (error) {
@@ -10042,9 +9913,10 @@ export class ClaudeAcpAgent {
// The entity is keyed by the summary message's uuid, which differs from
// the live id (the `compacting` status uuid) — replay is terminal-first
// by the RFD, and no client merges a replay into a live entity store.
// A subagent's own compaction summary is not the root session's, and a
// summary whose framing is unrecognized stays visible as transcript
// text rather than vanishing behind a summary-less entity.
// A subagent's own compaction summary is not the root session's. A summary
// whose framing is unrecognized is still this compaction, without summary
// text: isCompactSummary says what it is, and the framing is Claude Code's
// to change.
if (
replayCompactionUpdates &&
isCompactSummaryMessage(message) &&
@@ -10060,10 +9932,9 @@ export class ClaudeAcpAgent {
airClient: this.toolCallCapabilities.air.client,
},
);
if (replayCompaction.recordSummary(assistantMessageText(message.message))) {
await replayCompaction.finish(message.uuid, "completed");
return;
}
replayCompaction.recordSummary(assistantMessageText(message.message));
await replayCompaction.finish(message.uuid, "completed");
return;
}
for (const notification of toAcpNotifications(
@@ -10283,6 +10154,15 @@ export class ClaudeAcpAgent {
else if (!withdrawnByCancel) work.events.running();
}
/** Whether Claude is working in the session `sessionId`: an unsettled turn in
* its queue, or foreground work that no turn owns, such as a cycle that
* Claude Code started on its own. */
private claudeIsWorking(sessionId: string, session: Session): boolean {
return (
this.foregroundWork.has(sessionId) || (session.turnQueue ?? []).some((turn) => !turn.settled)
);
}
/** Sends the current title and `updatedAt` of the session `sessionId` as a
* `session_info_update` (see {@link SessionTitles.publishCurrent}). A
* failure is logged: the info is optional, so it does not fail a resume. */
@@ -11473,6 +11353,7 @@ export class ClaudeAcpAgent {
const newModelInfo = session.modelInfos.find((m) => m.value === value);
const modelChanged = session.models.currentModelId !== value;
if (modelChanged) {
session.idleCompaction?.disarm("model change");
// Seed the new model's context window WITHOUT awaited IPC on the
// switch path: cached authoritative value if we've already learned it
// (from a prior turn's `result.modelUsage`), else the text heuristic,
@@ -12079,15 +11960,18 @@ export class ClaudeAcpAgent {
settingsManager.getSettings().permissions?.disableBypassPermissionsMode !== "disable" &&
sessionMeta?.claudeCode?.options?.allowDangerouslySkipPermissions !== false;
const initialPermissionMode = resolveInitialPermissionMode(
{
explicit: creationOpts.permissionMode,
resumed: resumedPermissionMode,
defaultMode: settingsManager.getSettings().permissions?.defaultMode,
},
this.logger,
allowBypass,
);
const allowAuto = !autoModeDisabled(settingsManager.getSettings());
const { mode: initialPermissionMode, builtInDefault: builtInPermissionMode } =
resolveInitialPermissionMode(
{
explicit: creationOpts.permissionMode,
resumed: resumedPermissionMode,
defaultMode: settingsManager.getSettings().permissions?.defaultMode,
},
this.logger,
{ allowBypass, allowAuto },
);
const userProvidedOptions = sessionMeta?.claudeCode?.options;
const forwardSubagentText = this.forwardsSubagentText(params._meta);
@@ -12268,7 +12152,7 @@ export class ClaudeAcpAgent {
supportedDialogKinds: [REFUSAL_FALLBACK_DIALOG_KIND],
}
: {}),
pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE ?? (await claudeCliPath()),
pathToClaudeCodeExecutable: await claudeCliPath(),
extraArgs: {
...userProvidedOptions?.extraArgs,
"replay-user-messages": "",
@@ -12563,9 +12447,11 @@ export class ClaudeAcpAgent {
const { modes, autoModeFallbackWarningPending } = await this.sessionModes.initialize({
query: q,
requestedMode: initialPermissionMode,
builtInDefault: builtInPermissionMode,
currentModelInfo,
currentModelId: models.currentModelId,
allowBypass,
allowAuto,
});
timing.phase("modes");
@@ -12726,7 +12612,10 @@ export class ClaudeAcpAgent {
emittedToolCalls: new Set(),
eagerToolCallSessions: new Map(),
liveBackgroundTasks: new Map(),
reportsBackground: this.backgroundWork.enabled && options.perTaskStopAffordance === true,
background: new SessionBackgroundWork(
sessionId,
this.backgroundWork.enabled && options.perTaskStopAffordance === true,
),
stopsWorkflowsOnCancel:
options.perTaskStopAffordance === true &&
!this.backgroundWork.enabled &&
@@ -12746,6 +12635,27 @@ export class ClaudeAcpAgent {
)?.kind,
fileChangeReporter,
};
const registered = this.sessions[sessionId];
registered.idleCompaction = new IdleCompaction(
{
// `openUserInputs` also covers permission requests: `requestPermission`
// is wrapped in `withPendingUserInput`.
isBusy: () =>
this.claudeIsWorking(sessionId, registered) ||
(registered.openUserInputs?.size ?? 0) > 0,
contextTokens: () => registered.contextUsedTokens,
settings: () => registered.settingsManager.getSettings(),
compact: () =>
this.prompt(
{ sessionId, prompt: [{ type: "text", text: "/compact" }] },
"idleCompaction",
),
// The logger has no debug level.
log: (message) => this.logger.log(message),
},
// The CLI's own environment: process env, the client's and the provider's.
{ env },
);
timing.phase("register");
this.backgroundWork.sync(
sessionId,

158
src/acp-client.ts Normal file
View File

@@ -0,0 +1,158 @@
import {
AgentContext,
CompleteElicitationNotification,
CreateElicitationRequest,
CreateElicitationResponse,
methods,
ReadTextFileRequest,
ReadTextFileResponse,
RequestPermissionRequest,
RequestPermissionResponse,
SessionNotification,
WriteTextFileRequest,
WriteTextFileResponse,
} from "@agentclientprotocol/sdk/unstable";
import { AcpSessionNotification, asSdkSessionNotification } from "./acp-subagents.js";
import { type AcpPermissionRequest } from "./permissions/presentation.js";
import { ChangedMetaFilter } from "./tool-calls/changed-meta-filter.js";
/**
* Logger interface for customizing logging output
*/
export interface Logger {
log: (...args: any[]) => void;
error: (...args: any[]) => void;
/** Optional: a caller that supplies no `warn` gets its warnings on `error`. */
warn?: (...args: any[]) => void;
}
/**
* Client-facing surface the agent calls back into. This is the subset of ACP
* client methods the agent actually uses, expressed as a narrow interface so
* tests can supply lightweight mocks. In production it is backed by
* {@link ClientConnection} over the SDK's typed `AgentContext`.
*/
export interface AcpClient {
sessionUpdate(params: AcpSessionNotification): Promise<void>;
/** `signal`, when aborted, sends `$/cancel_request` for the in-flight
* permission request so the client can dismiss its prompt, and rejects the
* call at once: a client that ignores the cancel never answers. */
requestPermission(
params: AcpPermissionRequest,
signal?: AbortSignal,
): Promise<RequestPermissionResponse>;
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse>;
writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse>;
/** `signal`, when aborted, sends `$/cancel_request` for the in-flight
* elicitation so the client can dismiss its prompt, and rejects the call
* at once, as {@link requestPermission} does. */
createElicitation(
params: CreateElicitationRequest,
signal?: AbortSignal,
): Promise<CreateElicitationResponse>;
completeElicitation(params: CompleteElicitationNotification): Promise<void>;
/** Send a custom (extension) notification, e.g. `_claude/sdkMessage`. */
extNotification(method: string, params: Record<string, unknown>): Promise<void>;
}
/**
* Bridges {@link AcpClient} to the connection-scoped {@link AgentContext}
* exposed by `AgentApp.connect(...)` as `connection.client`. The peer handle is
* valid for the entire connection lifetime, so it is captured once at
* construction.
*/
export class ClientConnection implements AcpClient {
constructor(private readonly ctx: AgentContext) {}
sessionUpdate(params: AcpSessionNotification): Promise<void> {
return this.ctx.notify(methods.client.session.update, asSdkSessionNotification(params));
}
requestPermission(
params: RequestPermissionRequest,
signal?: AbortSignal,
): Promise<RequestPermissionResponse> {
return this.ctx.request(methods.client.session.requestPermission, params, {
cancellationSignal: signal,
stopWaitingOnCancel: true,
});
}
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
return this.ctx.request(methods.client.fs.readTextFile, params);
}
writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse> {
return this.ctx.request(methods.client.fs.writeTextFile, params);
}
createElicitation(
params: CreateElicitationRequest,
signal?: AbortSignal,
): Promise<CreateElicitationResponse> {
return this.ctx.request(methods.client.elicitation.create, params, {
cancellationSignal: signal,
stopWaitingOnCancel: true,
});
}
completeElicitation(params: CompleteElicitationNotification): Promise<void> {
return this.ctx.notify(methods.client.elicitation.complete, params);
}
extNotification(method: string, params: Record<string, unknown>): Promise<void> {
return this.ctx.notify(method, params);
}
}
/**
* The client that the agent talks to. For an AIR client, every session update
* passes the {@link ChangedMetaFilter} last, after the native subagent routing,
* so a `tool_call_update` carries only the `_meta` keys that changed. ACP
* merges only the top-level tool call fields, not the `_meta` keys. So another
* client gets the full `_meta` on each update.
*/
export class ChangedMetaClient implements AcpClient {
private readonly filter = new ChangedMetaFilter();
constructor(
private readonly inner: AcpClient,
private readonly airClient: () => boolean,
) {}
async sessionUpdate(params: AcpSessionNotification): Promise<void> {
if (!this.airClient()) return this.inner.sessionUpdate(params);
const update = this.filter.apply(params.update as SessionNotification["update"]);
if (update) await this.inner.sessionUpdate({ ...params, update } as AcpSessionNotification);
}
requestPermission(
params: AcpPermissionRequest,
signal?: AbortSignal,
): Promise<RequestPermissionResponse> {
return this.inner.requestPermission(params, signal);
}
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
return this.inner.readTextFile(params);
}
writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse> {
return this.inner.writeTextFile(params);
}
createElicitation(
params: CreateElicitationRequest,
signal?: AbortSignal,
): Promise<CreateElicitationResponse> {
return this.inner.createElicitation(params, signal);
}
completeElicitation(params: CompleteElicitationNotification): Promise<void> {
return this.inner.completeElicitation(params);
}
extNotification(method: string, params: Record<string, unknown>): Promise<void> {
return this.inner.extNotification(method, params);
}
}

19
src/agent-command.ts Normal file
View File

@@ -0,0 +1,19 @@
/**
* The command that starts this agent again with the flags it was started with,
* for a client to run in a terminal (e.g. `terminal-auth` login).
*
* Under Node, `process.argv` is `[node, script, ...flags]`, so the command is
* `node script ...flags`. In a `bun build --compile` executable it is
* `["bun", "/$bunfs/root/<name>", ...flags]` (`B:\~BUN\root\<name>` on
* Windows): `process.execPath` is the executable itself and `argv[1]` names a
* file inside it, which must not be passed back as an argument.
*/
export function agentCommand(argv: readonly string[] = process.argv): {
command: string;
args: string[];
} {
const [, script, ...flags] = argv;
if (script === undefined) return { command: process.execPath, args: [] };
const compiled = script.includes("$bunfs") || script.includes("~BUN");
return { command: process.execPath, args: compiled ? flags : [script, ...flags] };
}

View File

@@ -863,18 +863,29 @@ function recordPublished(task: AsyncTask, fields: Record<string, unknown>): void
* Recovers a background Bash task from the structured `backgroundTaskId` of
* 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
* those, `startedTaskOf` gives the background `local_bash` task whose
* `task_started` named the Bash call, which comes before the result.
*/
export function backgroundBashTaskFromToolResult(
content: unknown,
toolUseResult: unknown,
toolUses: Record<string, { name: string; input: unknown }>,
startedTaskOf?: (toolUseId: string) => string | undefined,
): (AsyncTaskStarted & { taskId: string; toolCallId: string }) | undefined {
if (!Array.isArray(content)) return undefined;
const toolResults = content.flatMap(toolResultBlock);
const bashResults = toolResults.filter((result) => toolUses[result.toolUseId]?.name === "Bash");
if (bashResults.length === 0) return undefined;
const backgroundResults = structuredBackgroundResults(toolUseResult, toolUses);
let backgroundResults = structuredBackgroundResults(toolUseResult, toolUses);
if (backgroundResults.length === 0 && startedTaskOf) {
backgroundResults = bashResults.flatMap((result) => {
const taskId = result.isError ? undefined : startedTaskOf(result.toolUseId);
return taskId ? [{ taskId, toolUseId: result.toolUseId }] : [];
});
}
const distinctTaskIds = new Set(backgroundResults.map((result) => result.taskId));
if (distinctTaskIds.size !== 1) return undefined;
const taskId = [...distinctTaskIds][0];

View File

@@ -210,6 +210,15 @@ export function fromCliStatus(stdout: string, apiKeyHelper?: string): AuthStatus
if (!externalBackend && !status.loggedIn && !status.apiKeySource) {
return notLoggedInAuthStatus();
}
if (
status.loggedIn &&
status.authMethod === "oauth_token" &&
status.apiProvider === "firstParty" &&
!status.apiKeySource &&
!status.subscriptionType
) {
return { kind: "account", label: "Claude subscription", detail: "OAuth token" };
}
return mapAuthFields({
apiProvider: status.apiProvider,
subscriptionType: status.subscriptionType,

131
src/background-output.ts Normal file
View File

@@ -0,0 +1,131 @@
/**
* The output of a command that went to the background, read from the file
* that Claude Code writes it to. Claude Code streams no output of such a
* command, so its display terminal would show only the placeholder of the
* Bash result ("Command running in background with ID …") until the command
* exits. ACP v2 asks an Agent to keep a listed terminal's output current
* whenever it can read it (the v2 prompt lifecycle, "Background work").
*
* The file is polled: watching a file is not reliable on every platform, and
* a missing file only means that nothing was written yet.
*/
import { open } from "node:fs/promises";
import { StringDecoder } from "node:string_decoder";
/** How often a running command's output file is read. */
export const BACKGROUND_OUTPUT_POLL_MS = 500;
/** The most bytes one read takes, so a burst of output does not block the loop. */
export const BACKGROUND_OUTPUT_READ_BYTES = 256 * 1024;
/**
* The most unread bytes that are still sent. A command that writes faster
* than the polls read falls behind. Past this, the reads skip to the file's
* last bytes, as a snapshot: a terminal shows the latest output, and a
* client keeps a bounded amount of it anyway.
*/
export const BACKGROUND_OUTPUT_MAX_BACKLOG_BYTES = 1024 * 1024;
/**
* Text that the file adds. A snapshot replaces the terminal's output so far:
* the first one replaces the placeholder of the Bash result, and a later one
* follows a skip or a file that shrank. Other text appends to it.
*/
export type BackgroundOutput = { text: string; replaces: boolean };
/**
* Follows the output file `path` of one background command and hands each
* piece of new text to `publish`, in order. Reads never overlap. A read that
* fails, for example because the file does not exist yet, is tried again at
* the next poll. `publish` must not reject: the next read waits for it.
*/
export class BackgroundOutputTail {
private offset = 0;
private decoder = new StringDecoder("utf8");
/** Whether the next text replaces the terminal's output. */
private replaces = true;
private stopped = false;
private reading: Promise<void> = Promise.resolve();
private readonly timer: ReturnType<typeof setInterval>;
constructor(
private readonly path: string,
private readonly publish: (output: BackgroundOutput) => Promise<void>,
) {
this.timer = setInterval(() => void this.read(false), BACKGROUND_OUTPUT_POLL_MS);
// A command that runs on must not keep the agent process alive.
this.timer.unref();
}
/**
* Reads the rest of the file and stops. The command ended, so what the file
* holds is its whole output, and it goes out before the terminal's exit.
*/
async finish(): Promise<void> {
clearInterval(this.timer);
await this.read(true);
this.stopped = true;
}
/** Stops without reading more: the session ended. */
stop(): void {
clearInterval(this.timer);
this.stopped = true;
}
/**
* Reads what the file adds, one read of at most
* {@link BACKGROUND_OUTPUT_READ_BYTES} at a time, and publishes each read's
* text before the next. `toEnd` reads until the end of the file, and ends a
* character that the command cut off with a replacement character.
*/
private read(toEnd: boolean): Promise<void> {
this.reading = this.reading.then(async () => {
if (this.stopped) return;
let file;
try {
file = await open(this.path, "r");
} catch {
// Nothing written yet, or the file is gone: try again at the next
// poll. At the end, the terminal keeps the output it has.
return;
}
try {
const { size } = await file.stat();
// A file that shrank is a new one: its output starts over. Past the
// backlog, the reads skip to the file's last bytes.
const start = size < this.offset ? 0 : this.offset;
const skipTo = Math.max(start, size - BACKGROUND_OUTPUT_MAX_BACKLOG_BYTES);
if (skipTo !== this.offset) {
this.offset = skipTo;
this.decoder = new StringDecoder("utf8");
this.replaces = true;
}
const buffer = Buffer.alloc(BACKGROUND_OUTPUT_READ_BYTES);
let bytesRead: number;
do {
({ bytesRead } = await file.read(buffer, 0, buffer.length, this.offset));
this.offset += bytesRead;
const text = this.decoder.write(buffer.subarray(0, bytesRead));
if (text !== "") await this.send(text);
} while (toEnd && bytesRead > 0);
if (toEnd) {
const rest = this.decoder.end();
// A command that wrote nothing still replaces the placeholder.
if (rest !== "" || this.replaces) await this.send(rest);
}
} catch {
// The read failed: try again at the next poll.
} finally {
await file.close();
}
});
return this.reading;
}
private async send(text: string): Promise<void> {
const replaces = this.replaces;
this.replaces = false;
await this.publish({ text, replaces });
}
}

View File

@@ -5,8 +5,9 @@
* "Background work").
*
* The lists are built from Claude Code's live background task set
* (`background_tasks_changed`), without the tasks it marks `ambient`, which
* are housekeeping that it started on its own. The root session and each
* (`background_tasks_changed`). Claude Code marks `ambient` its own
* housekeeping and every live-update watcher; those are left out, except a
* task that a tool call started, such as a `Monitor` call's. The root session and each
* announced subagent session (the subagents RFD form) has its own list. Each
* task is listed once, in the list of the session that runs it:
*
@@ -30,6 +31,7 @@ import { RequestError } from "@agentclientprotocol/sdk/unstable";
import type { AcpSessionNotification } from "./acp-subagents.js";
import type { NativeSubagent } from "./native-subagents.js";
import { type AcpToolCallRenderer, V2_CANCELLED, V2_PAUSED } from "./tool-calls/renderer.js";
import { type BackgroundOutput, BackgroundOutputTail } from "./background-output.js";
/** An item of a `background` list. */
export type BackgroundItem =
@@ -38,6 +40,18 @@ export type BackgroundItem =
| { type: "subagent"; sessionId: string }
| { type: "_claude/task"; taskId: string; description: string };
/** The display terminal of a command that went to the background (see `Session.backgroundTerminals`). */
export interface BackgroundTerminal {
/** The Bash tool call, whose id names the terminal. */
toolCallId: string;
/** The subagent tool call that ran the command, if any. */
parentToolUseId?: string;
/** The file that Claude Code writes the command's output to, from the Bash result. */
outputFilePath?: string;
/** Reads that file into the terminal while the command runs. */
output?: BackgroundOutputTail;
}
/** A tool call whose task runs on in the background (see `Session.backgroundToolCalls`). */
export interface BackgroundToolCall {
toolCallId: string;
@@ -78,223 +92,452 @@ export interface BackgroundSink {
forget(sessionId: string): void;
}
/** The state of a session that its lists are built from. */
export interface BackgroundWorkSession {
reportsBackground?: boolean;
backgroundLevel?: BackgroundLevel;
backgroundTerminals?: Map<string, { toolCallId: string; parentToolUseId?: string }>;
backgroundToolCalls?: Map<string, BackgroundToolCall>;
/**
* The state of a session that other modules own, and that the background
* work reads: Claude Code's tasks as the consumer tracks them, the subagent
* sessions, and the query that stops a task.
*/
export interface BackgroundOwner {
liveBackgroundTasks: Map<string, { parentToolUseId?: string; isSubagent: boolean }>;
nativeSubagentsByTaskId?: Map<string, NativeSubagent>;
liveBackgroundTasks?: Map<string, { parentToolUseId?: string; isSubagent: boolean }>;
/** The tool uses whose result has not come yet, by id. */
toolUseCache?: Record<string, unknown>;
query: { stopTask(taskId: string): Promise<void> };
}
/** A task of a `background_tasks_changed` payload. */
export type LevelTask = {
task_id: string;
task_type: string;
description: string;
parent_task_id?: string;
ambient?: boolean;
};
/** The most returned tool calls that {@link SessionBackgroundWork} remembers. */
const MAX_RETURNED_CALLS = 1024;
/** The tools whose structured result names the task that the call started. */
const TASK_TOOLS = new Set(["Monitor", "Workflow"]);
/**
* Registers each tool call of a tool result message whose task goes on in
* the background: a `Monitor` or `Workflow` call, whose structured result
* names its task, or a call that a live task names as its tool use, such as
* a backgrounded MCP call. The task must be in the level: one that ended
* already, or that Claude Code marks `ambient`, leaves its call as it is.
* Returns the ids of the calls, whose result then reports `in_progress` (see
* `withRunningToolCall`).
* The background work of one root session, and of its subagent sessions: the
* terminals and tool calls of the tasks that run in the background, and what
* the client is told of them.
*
* Terminals are kept for every client that renders them, as before the
* background lists. The rest is kept only when `reported`.
*/
export function registerBackgroundToolCalls(
session: BackgroundWorkSession,
content: unknown,
toolUseResult: unknown,
toolUses: Record<string, { name: string; input: unknown }>,
parentToolUseId: string | null,
): Set<string> {
const registered = new Set<string>();
if (!session.reportsBackground || !Array.isArray(content)) return registered;
const results = content.filter(
(block): block is { type: "tool_result"; tool_use_id: string; is_error?: boolean } =>
typeof block === "object" &&
block !== null &&
block.type === "tool_result" &&
typeof block.tool_use_id === "string" &&
block.is_error !== true,
);
const structuredTaskId =
results.length === 1 &&
typeof toolUseResult === "object" &&
toolUseResult !== null &&
typeof (toolUseResult as { taskId?: unknown }).taskId === "string"
? (toolUseResult as { taskId: string }).taskId
: undefined;
for (const { tool_use_id: toolCallId } of results) {
const toolUse = toolUses[toolCallId];
if (!toolUse) continue;
const taskId =
(TASK_TOOLS.has(toolUse.name) ? structuredTaskId : undefined) ??
[...(session.liveBackgroundTasks ?? [])].find(
([, task]) => task.parentToolUseId === toolCallId && !task.isSubagent,
)?.[0];
const task = taskId === undefined ? undefined : session.backgroundLevel?.get(taskId);
if (
!task ||
task.ambient ||
task.taskType === "local_bash" ||
task.taskType === "local_agent"
) {
continue;
}
(session.backgroundToolCalls ??= new Map()).set(taskId!, {
toolCallId,
toolUse,
...(parentToolUseId ? { parentToolUseId } : {}),
});
registered.add(toolCallId);
export class SessionBackgroundWork {
/** Claude Code's live background task set, from its last `background_tasks_changed`. */
private level: BackgroundLevel = new Map();
/** The terminals of the commands that went to the background, by task id. */
private readonly terminals = new Map<string, BackgroundTerminal>();
/** The tool calls whose tasks went on in the background, by task id. */
private readonly toolCalls = new Map<string, BackgroundToolCall>();
/**
* The tool calls that returned, oldest first. A task that a call started
* is part of that call until it returns, and the foreground work waits on
* it. The task's `task_started` can come after the call returned. Bounded
* by {@link MAX_RETURNED_CALLS}: a task joins the level around the time its
* call returns.
*/
private readonly returned = new Set<string>();
/**
* `reported`: whether the client gets the lists. Only when Claude Code's
* interrupt spares background tasks, as `perTaskStopAffordance` makes it.
*/
constructor(
private readonly sessionId: string,
readonly reported: boolean,
) {}
/**
* Keeps the display terminal of the command `taskId` running after its Bash
* result, until its task ends (see `withRunningTerminal`).
*/
terminalStarted(taskId: string, terminal: BackgroundTerminal): void {
this.terminals.set(taskId, terminal);
}
return registered;
}
/** The level of a `background_tasks_changed` payload. */
export function backgroundLevel(
tasks: {
task_id: string;
task_type: string;
description: string;
parent_task_id?: string;
ambient?: boolean;
}[],
): BackgroundLevel {
return new Map(
tasks.map((task) => [
task.task_id,
{
ambient: task.ambient === true,
taskType: task.task_type,
description: task.description,
...(task.parent_task_id !== undefined ? { parentTaskId: task.parent_task_id } : {}),
},
]),
);
}
/**
* Reads what a tool result message tells of the background tasks:
*
* - The returned calls no longer hold the tasks that they started.
* - Each call whose task goes on in the background is registered: a
* `Monitor` or `Workflow` call, whose structured result names its task,
* or a call that a live task names as its tool use, such as a
* backgrounded MCP call. The task must be in the level: one that ended
* already leaves its call as it is. Claude Code marks every live-update
* watcher `ambient`, including the task of a `Monitor` call; a call
* started it, so it is work the client can see and stop, and is
* registered all the same. Ambient tasks without a call of their own,
* such as watchers Claude Code starts by itself, are never registered.
*
* Returns the ids of the registered calls, whose result then reports
* `in_progress` (see `withRunningToolCall`).
*/
toolResults(
owner: BackgroundOwner,
content: unknown,
toolUseResult: unknown,
toolUses: Record<string, { name: string; input: unknown }>,
parentToolUseId: string | null,
): Set<string> {
const registered = new Set<string>();
if (!this.reported || !Array.isArray(content)) return registered;
const results = content.filter(
(block): block is { type: "tool_result"; tool_use_id: string; is_error?: boolean } =>
typeof block === "object" &&
block !== null &&
block.type === "tool_result" &&
typeof block.tool_use_id === "string",
);
for (const { tool_use_id: toolCallId } of results) {
this.returned.add(toolCallId);
if (this.returned.size > MAX_RETURNED_CALLS) {
this.returned.delete(this.returned.values().next().value!);
}
}
const succeeded = results.filter((block) => block.is_error !== true);
const structuredTaskId =
succeeded.length === 1 &&
typeof toolUseResult === "object" &&
toolUseResult !== null &&
typeof (toolUseResult as { taskId?: unknown }).taskId === "string"
? (toolUseResult as { taskId: string }).taskId
: undefined;
for (const { tool_use_id: toolCallId } of succeeded) {
const toolUse = toolUses[toolCallId];
if (!toolUse) continue;
const taskId =
(TASK_TOOLS.has(toolUse.name) ? structuredTaskId : undefined) ??
[...owner.liveBackgroundTasks].find(
([, task]) => task.parentToolUseId === toolCallId && !task.isSubagent,
)?.[0];
const task = taskId === undefined ? undefined : this.level.get(taskId);
if (!task || task.taskType === "local_bash" || task.taskType === "local_agent") {
continue;
}
this.toolCalls.set(taskId!, {
toolCallId,
toolUse,
...(parentToolUseId ? { parentToolUseId } : {}),
});
registered.add(toolCallId);
}
return registered;
}
/**
* The `background` list of the root session `rootSessionId` and of each of
* its announced subagent sessions, by session id, deeper sessions first and
* the root last. Each list is empty while
* the level has not been reported: Claude Code sends none before the first
* background task starts.
*/
export function backgroundLists(
session: BackgroundWorkSession,
rootSessionId: string,
): Map<string, BackgroundItem[]> {
const children = [...(session.nativeSubagentsByTaskId ?? [])].filter(
([, child]) => child.announced,
);
const childByTask = new Map(children);
const childByToolUse = new Map(
children.flatMap(([, child]) =>
child.parentToolUseId ? [[child.parentToolUseId, child] as const] : [],
),
);
const lists = new Map<string, BackgroundItem[]>([
[rootSessionId, []],
...children.map(([, child]): [string, BackgroundItem[]] => [child.sessionId, []]),
]);
const level: BackgroundLevel = session.backgroundLevel ?? new Map();
for (const [taskId, task] of level) {
if (task.ambient) continue;
/**
* Starts reading the output of the background command `taskId` into its
* terminal, when the client gets the lists and the Bash result named the
* command's output file. `send` sends an update of the terminal's tool
* call, routed as the call's other updates. Called once the Bash result's
* own updates are sent: the first output replaces the placeholder that they
* gave the terminal.
*/
followOutput(taskId: string, send: Send): void {
const terminal = this.terminals.get(taskId);
if (!this.reported || !terminal?.outputFilePath || terminal.output) return;
terminal.output = new BackgroundOutputTail(terminal.outputFilePath, (output) =>
send(terminalOutputUpdate(this.sessionId, terminal, output)),
);
}
/**
* The task `taskId` ended. Reads the rest of its command's output, then
* exits its terminal, and gives a tool call whose task it was its final
* status: `cancelled` for a task that Claude Code stopped, and `completed`
* for one that ended without saying how, such as one that the level
* dropped: it no longer runs, and reported no failure. Claude Code reports
* no exit code for a command, and `completed` does not mean 0: Claude Code
* accepts some non-zero codes as a success, such as grep's 1.
*/
async taskEnded(taskId: string, end: BackgroundTaskEnd, send: Send): Promise<void> {
const terminal = this.terminals.get(taskId);
if (terminal) {
await terminal.output?.finish();
this.terminals.delete(taskId);
await send(terminalExit(this.sessionId, terminal));
}
const toolCall = this.toolCalls.get(taskId);
if (toolCall) {
this.toolCalls.delete(taskId);
await send(
toolCallStatusUpdate(
this.sessionId,
toolCall,
end === "stopped" ? V2_CANCELLED : end === "failed" ? "failed" : "completed",
),
);
}
}
/**
* The update that reports the background tool call of the task `taskId` as
* `paused`, or as `in_progress` again, when `paused` changes it. Claude Code
* pauses a workflow this way.
*/
taskPaused(taskId: string, paused: boolean): AcpSessionNotification | undefined {
const toolCall = this.toolCalls.get(taskId);
if (!toolCall || (toolCall.paused ?? false) === paused) return undefined;
toolCall.paused = paused;
return toolCallStatusUpdate(this.sessionId, toolCall, paused ? V2_PAUSED : "in_progress");
}
/**
* Takes Claude Code's new live background task set. The terminals and tool
* calls whose tasks it no longer holds end first, before a list drops them,
* as v2 requires. A terminal or tool call is registered at its tool result,
* after its task joined the level, so a later level without the task means
* that it ended. Claude Code usually sends a task's end before that level,
* which ends them first, but the SDK does not promise that order.
*/
async levelChanged(tasks: LevelTask[], send: Send): Promise<void> {
if (!this.reported) return;
const level: BackgroundLevel = new Map(
tasks.map((task) => [
task.task_id,
{
ambient: task.ambient === true,
taskType: task.task_type,
description: task.description,
...(task.parent_task_id !== undefined ? { parentTaskId: task.parent_task_id } : {}),
},
]),
);
for (const taskId of [...this.terminals.keys(), ...this.toolCalls.keys()]) {
if (!level.has(taskId)) await this.taskEnded(taskId, undefined, send);
}
this.level = level;
}
/**
* The query closed: Claude Code's background tasks end with its process.
* Stops reading output, and returns the updates that exit the root
* session's terminals and cancel its tool calls. The level empties, so the
* next lists are empty. A subagent's command or tool call is left as it
* was: it is on the child's stream, which the closed query's routing owned.
*/
queryClosed(): AcpSessionNotification[] {
for (const terminal of this.terminals.values()) terminal.output?.stop();
if (!this.reported) return [];
const updates: AcpSessionNotification[] = [];
for (const [taskId, terminal] of this.terminals) {
if (terminal.parentToolUseId) continue;
this.terminals.delete(taskId);
updates.push(terminalExit(this.sessionId, terminal));
}
for (const [taskId, toolCall] of this.toolCalls) {
if (toolCall.parentToolUseId) continue;
this.toolCalls.delete(taskId);
updates.push(toolCallStatusUpdate(this.sessionId, toolCall, V2_CANCELLED));
}
this.level = new Map();
this.returned.clear();
return updates;
}
/**
* The `background` list of the root session and of each of its announced
* subagent sessions, with what their repeated states need, deeper sessions
* first and the root last: a child's own list goes out before its parent's
* list drops it. Each list is empty while the level has not been reported:
* Claude Code sends none before the first background task starts.
*/
reports(owner: BackgroundOwner, rootState: WorkStateName): BackgroundReport[] {
const children = [...(owner.nativeSubagentsByTaskId ?? [])].filter(
([, child]) => child.announced,
);
const childByTask = new Map(children);
const childByToolUse = new Map(
children.flatMap(([, child]) =>
child.parentToolUseId ? [[child.parentToolUseId, child] as const] : [],
),
);
const lists = new Map<string, BackgroundItem[]>([
[this.sessionId, []],
...children.map(([, child]): [string, BackgroundItem[]] => [child.sessionId, []]),
]);
// A subagent's command or tool call runs on the child's stream.
const ownerOf = (parentToolUseId: string | undefined) =>
parentToolUseId ? childByToolUse.get(parentToolUseId)?.sessionId : rootSessionId;
const terminal = session.backgroundTerminals?.get(taskId);
if (terminal) {
const owner = ownerOf(terminal.parentToolUseId);
if (owner) lists.get(owner)!.push({ type: "terminal", terminalId: terminal.toolCallId });
continue;
parentToolUseId ? childByToolUse.get(parentToolUseId)?.sessionId : this.sessionId;
for (const [taskId, task] of this.level) {
// A registered call's task is listed even when ambient, as a `Monitor`
// call's is (see `toolResults`).
const toolCall = this.toolCalls.get(taskId);
if (toolCall) {
const session = ownerOf(toolCall.parentToolUseId);
if (session)
lists.get(session)!.push({ type: "tool_call", toolCallId: toolCall.toolCallId });
continue;
}
if (task.ambient) continue;
const terminal = this.terminals.get(taskId);
if (terminal) {
const session = ownerOf(terminal.parentToolUseId);
if (session)
lists.get(session)!.push({ type: "terminal", terminalId: terminal.toolCallId });
continue;
}
// A command without a terminal yet is one that foreground work waits on.
// A subagent is listed below, as its session.
if (task.taskType === "local_bash" || task.taskType === "local_agent") continue;
// A task joins the level before its `task_started`, and before the tool
// call that starts it returns. Until both, the task is unknown or part
// of that tool call.
const live = owner.liveBackgroundTasks.get(taskId);
if (!live || (live.parentToolUseId && !this.returned.has(live.parentToolUseId))) continue;
const parent = task.parentTaskId ? childByTask.get(task.parentTaskId) : undefined;
lists
.get(parent?.sessionId ?? this.sessionId)!
.push({ type: "_claude/task", taskId, description: task.description });
}
const toolCall = session.backgroundToolCalls?.get(taskId);
if (toolCall) {
const owner = ownerOf(toolCall.parentToolUseId);
if (owner) lists.get(owner)!.push({ type: "tool_call", toolCallId: toolCall.toolCallId });
continue;
// A child is listed in its parent's list while it works in the background,
// and while it is idle with background work of its own. Deeper children
// first, so a child's list is whole before its parent's reads it.
const sessions = new Map(children.map(([, child]) => [child.sessionId, child]));
const depth = (child: NativeSubagent): number => {
const parent = sessions.get(child.parentSessionId);
return parent ? depth(parent) + 1 : 0;
};
const ordered = children.sort(([, a], [, b]) => depth(b) - depth(a));
for (const [taskId, child] of ordered) {
const task = this.level.get(taskId);
const working =
child.workState !== undefined &&
child.workState !== "idle" &&
task !== undefined &&
!task.ambient;
if (working || lists.get(child.sessionId)!.length > 0) {
lists.get(child.parentSessionId)?.push({ type: "subagent", sessionId: child.sessionId });
}
}
// A command without a terminal yet is one that foreground work waits on.
// A subagent is listed below, as its session.
if (task.taskType === "local_bash" || task.taskType === "local_agent") continue;
// A task joins the level before its `task_started`, and before the tool
// call that starts it returns. Until both, the task is unknown or part of
// that tool call, which the foreground work waits on. Claude Code drops a
// tool use from its cache at the tool's result.
const live = session.liveBackgroundTasks?.get(taskId);
if (!live || (live.parentToolUseId && session.toolUseCache?.[live.parentToolUseId])) {
continue;
}
const parent = task.parentTaskId ? childByTask.get(task.parentTaskId) : undefined;
lists
.get(parent?.sessionId ?? rootSessionId)!
.push({ type: "_claude/task", taskId, description: task.description });
return [
...ordered.map(([, child]) => ({
sessionId: child.sessionId,
items: lists.get(child.sessionId)!,
state: workStateName(child.workState),
parentSessionId: child.parentSessionId,
})),
{ sessionId: this.sessionId, items: lists.get(this.sessionId)!, state: rootState },
];
}
// A child is listed in its parent's list while it works in the background,
// and while it is idle with background work of its own. Deeper children
// first, so a child's list is whole before its parent's reads it.
const sessions = new Map(children.map(([, child]) => [child.sessionId, child]));
const depth = (child: NativeSubagent): number => {
const parent = sessions.get(child.parentSessionId);
return parent ? depth(parent) + 1 : 0;
};
for (const [taskId, child] of children.sort(([, a], [, b]) => depth(b) - depth(a))) {
const task = level.get(taskId);
const working =
child.workState !== undefined &&
child.workState !== "idle" &&
task !== undefined &&
!task.ambient;
if (working || lists.get(child.sessionId)!.length > 0) {
lists.get(child.parentSessionId)?.push({ type: "subagent", sessionId: child.sessionId });
}
/**
* The current state of each background tool call in `toolCallIds`: the
* call as `renderer` shows its tool use, with its status now (see
* `ClaudeAcpAgent.publishBackgroundToolCalls`).
*/
toolCallStates(
toolCallIds: string[],
renderer: AcpToolCallRenderer,
cwd: string,
): AcpSessionNotification[] {
return [...this.toolCalls.values()]
.filter((toolCall) => toolCallIds.includes(toolCall.toolCallId))
.map((toolCall) => {
const update = renderer.toolCall({ id: toolCall.toolCallId, ...toolCall.toolUse }, { cwd });
const status = toolCall.paused ? V2_PAUSED : "in_progress";
return {
sessionId: this.sessionId,
update: { ...update, status } as AcpSessionNotification["update"],
};
});
}
/**
* The task of the terminal or tool call `target` that the session
* `sessionId` lists, or undefined when it lists no such item.
*/
listedTask(
owner: BackgroundOwner,
sessionId: string,
target: BackgroundTarget,
): string | undefined {
if (!this.reported) return undefined;
const listed = this.reports(owner, "idle")
.find((report) => report.sessionId === sessionId)
?.items.some((item) =>
"terminalId" in target
? item.type === "terminal" && item.terminalId === target.terminalId
: item.type === "tool_call" && item.toolCallId === target.toolCallId,
);
if (!listed) return undefined;
const id = "terminalId" in target ? target.terminalId : target.toolCallId;
const entries = "terminalId" in target ? this.terminals : this.toolCalls;
return [...entries].find(([, entry]) => entry.toolCallId === id)?.[0];
}
// In the same order, the root last: a child's own list goes out before its
// parent's list drops it.
return new Map([
...children.map(([, child]): [string, BackgroundItem[]] => [
child.sessionId,
lists.get(child.sessionId)!,
]),
[rootSessionId, lists.get(rootSessionId)!],
]);
}
/**
* The tasks of the terminals and tool calls that `level` does not hold:
* Claude Code no longer runs them, so they end before a list drops them, as
* v2 requires. A terminal or tool call is registered at its tool result,
* after its task joined the level, so a later level without the task means
* that it ended. Claude Code usually sends a task's end before that level,
* which ends them first, but the SDK does not promise that order.
*/
export function droppedTasks(session: BackgroundWorkSession, level: BackgroundLevel): string[] {
return [
...(session.backgroundTerminals?.keys() ?? []),
...(session.backgroundToolCalls?.keys() ?? []),
].filter((taskId) => !level.has(taskId));
}
type Send = (notification: AcpSessionNotification) => Promise<void>;
/** A terminal or tool call that `terminal/cancel` or `tool_call/cancel` names. */
export type BackgroundTarget = { terminalId: string } | { toolCallId: string };
/**
* Removes the terminal of the background command `taskId` of the session
* `sessionId`, and returns the update that exits it, or undefined when the
* command has no terminal. Claude Code reports no exit code for it, and
* `completed` does not mean 0: Claude Code accepts some non-zero codes as a
* success, such as grep's 1.
* Stops the task of a terminal or tool call that the session `sessionId`
* lists (v2 `terminal/cancel` and `tool_call/cancel`). `sessions` are the
* root sessions: a subagent session's list is built with its root's. The
* terminal exits, or the tool call gets its final status, when Claude Code
* reports the task stopped.
*/
export function takeBackgroundTerminalExit(
session: BackgroundWorkSession,
export async function cancelBackgroundWork(
sessions: Array<BackgroundOwner & { background?: SessionBackgroundWork }>,
sessionId: string,
taskId: string,
): AcpSessionNotification | undefined {
const terminal = session.backgroundTerminals?.get(taskId);
if (!terminal) return undefined;
session.backgroundTerminals!.delete(taskId);
target: BackgroundTarget,
): Promise<void> {
const notCancellable = () =>
"terminalId" in target
? RequestError.invalidParams(
{ sessionId, terminalId: target.terminalId },
`Terminal ${target.terminalId} is not a command running in the background, so it cannot be cancelled.`,
)
: RequestError.invalidParams(
{ sessionId, toolCallId: target.toolCallId },
`Tool call ${target.toolCallId} is not running in the background, so it cannot be cancelled.`,
);
for (const session of sessions) {
const taskId = session.background?.listedTask(session, sessionId, target);
if (taskId === undefined) continue;
try {
await session.query.stopTask(taskId);
} catch (error) {
// The task ended meanwhile: it has ended on the client too, or is about to.
if (session.background?.listedTask(session, sessionId, target) === undefined) {
throw notCancellable();
}
throw error;
}
return;
}
throw notCancellable();
}
/** The update that gives the terminal of `terminal` the text `output`. */
function terminalOutputUpdate(
sessionId: string,
terminal: BackgroundTerminal,
output: BackgroundOutput,
): AcpSessionNotification {
const data = { terminal_id: terminal.toolCallId, data: output.text };
return {
sessionId,
update: {
sessionUpdate: "tool_call_update",
toolCallId: terminal.toolCallId,
_meta: {
...(terminal.parentToolUseId
? { claudeCode: { parentToolUseId: terminal.parentToolUseId } }
: {}),
...(output.replaces ? { terminal_output: data } : { terminal_output_delta: data }),
},
},
};
}
/** The update that exits the terminal of `terminal`, with no exit code. */
function terminalExit(sessionId: string, terminal: BackgroundTerminal): AcpSessionNotification {
return {
sessionId,
update: {
@@ -310,69 +553,6 @@ export function takeBackgroundTerminalExit(
};
}
/**
* Removes the background tool call of the task `taskId` of the session
* `sessionId`, and returns the update that gives it its final status, or
* undefined when the task has no such call. A task that Claude Code stopped
* is `cancelled`. One that ended without saying how, such as one that the
* level dropped, is `completed`: it no longer runs, and reported no failure.
*/
export function takeBackgroundToolCallEnd(
session: BackgroundWorkSession,
sessionId: string,
taskId: string,
end: BackgroundTaskEnd,
): AcpSessionNotification | undefined {
const toolCall = session.backgroundToolCalls?.get(taskId);
if (!toolCall) return undefined;
session.backgroundToolCalls!.delete(taskId);
return toolCallStatusUpdate(
sessionId,
toolCall,
end === "stopped" ? V2_CANCELLED : end === "failed" ? "failed" : "completed",
);
}
/**
* The update that reports the background tool call of the task `taskId` as
* `paused`, or as `in_progress` again, when `paused` changes it. Claude Code
* pauses a workflow this way.
*/
export function backgroundToolCallPause(
session: BackgroundWorkSession,
sessionId: string,
taskId: string,
paused: boolean,
): AcpSessionNotification | undefined {
const toolCall = session.backgroundToolCalls?.get(taskId);
if (!toolCall || (toolCall.paused ?? false) === paused) return undefined;
toolCall.paused = paused;
return toolCallStatusUpdate(sessionId, toolCall, paused ? V2_PAUSED : "in_progress");
}
/**
* The current state of each background tool call in `toolCallIds` of the
* root session `sessionId`: the call as `renderer` shows its tool use, with
* its status now (see `ClaudeAcpAgent.publishBackgroundToolCalls`).
*/
export function backgroundToolCallStates(
session: BackgroundWorkSession & { cwd: string },
sessionId: string,
toolCallIds: string[],
renderer: AcpToolCallRenderer,
): AcpSessionNotification[] {
return [...(session.backgroundToolCalls?.values() ?? [])]
.filter((toolCall) => toolCallIds.includes(toolCall.toolCallId))
.map((toolCall) => {
const update = renderer.toolCall(
{ id: toolCall.toolCallId, ...toolCall.toolUse },
{ cwd: session.cwd },
);
const status = toolCall.paused ? V2_PAUSED : "in_progress";
return { sessionId, update: { ...update, status } as AcpSessionNotification["update"] };
});
}
function toolCallStatusUpdate(
sessionId: string,
toolCall: BackgroundToolCall,
@@ -393,80 +573,6 @@ function toolCallStatusUpdate(
};
}
/**
* Ends the background work of a session whose query closed: Claude Code's
* background tasks end with its process. Returns the updates that exit the
* root session's terminals and cancel its tool calls, and empties the level,
* so the next lists are empty. A subagent's command or tool call is left as
* it was: it is on the child's stream, which the closed query's routing
* owned.
*/
export function endBackgroundWork(
session: BackgroundWorkSession,
sessionId: string,
): AcpSessionNotification[] {
if (!session.reportsBackground) return [];
const exits = [...(session.backgroundTerminals ?? [])].flatMap(([taskId, terminal]) => {
const exit = terminal.parentToolUseId
? undefined
: takeBackgroundTerminalExit(session, sessionId, taskId);
return exit ? [exit] : [];
});
const ends = [...(session.backgroundToolCalls ?? [])].flatMap(([taskId, toolCall]) => {
const end = toolCall.parentToolUseId
? undefined
: takeBackgroundToolCallEnd(session, sessionId, taskId, "stopped");
return end ? [end] : [];
});
session.backgroundLevel = undefined;
return [...exits, ...ends];
}
/**
* Stops the task of a terminal or tool call that the session `sessionId`
* lists (v2 `terminal/cancel` and `tool_call/cancel`). `sessions` are the
* root sessions, by id: a subagent session's list is built with its root's.
* The terminal exits, or the tool call gets its final status, when Claude
* Code reports the task stopped.
*/
export async function cancelBackgroundWork(
sessions: Record<string, BackgroundWorkSession>,
sessionId: string,
target: { terminalId: string } | { toolCallId: string },
): Promise<void> {
const notCancellable = () =>
"terminalId" in target
? RequestError.invalidParams(
{ sessionId, terminalId: target.terminalId },
`Terminal ${target.terminalId} is not a command running in the background, so it cannot be cancelled.`,
)
: RequestError.invalidParams(
{ sessionId, toolCallId: target.toolCallId },
`Tool call ${target.toolCallId} is not running in the background, so it cannot be cancelled.`,
);
const isTarget = (item: BackgroundItem) =>
"terminalId" in target
? item.type === "terminal" && item.terminalId === target.terminalId
: item.type === "tool_call" && item.toolCallId === target.toolCallId;
const listed = (root: string, session: BackgroundWorkSession) =>
session.reportsBackground === true &&
(backgroundLists(session, root).get(sessionId)?.some(isTarget) ?? false);
const owner = Object.entries(sessions).find(([root, session]) => listed(root, session));
if (!owner) throw notCancellable();
const [root, session] = owner;
const id = "terminalId" in target ? target.terminalId : target.toolCallId;
const taskId = [
...(("terminalId" in target ? session.backgroundTerminals : session.backgroundToolCalls) ?? []),
].find(([, entry]) => entry.toolCallId === id)![0];
try {
await session.query.stopTask(taskId);
} catch (error) {
// The task ended meanwhile: it has ended on the client too, or is about to.
if (!listed(root, session)) throw notCancellable();
throw error;
}
}
/**
* Publishes the lists of each session that reports them, each time one
* changes. The sink sends them to the client synchronously, so a list follows
@@ -488,25 +594,17 @@ export class BackgroundWorkReporter {
* first call for a session publishes its list, also when it is empty.
* `rootState` is the root session's state now.
*/
sync(rootSessionId: string, session: BackgroundWorkSession, rootState: WorkStateName): void {
if (!this.sink || !session.reportsBackground) return;
const children = new Map(
[...(session.nativeSubagentsByTaskId?.values() ?? [])].map((child) => [
child.sessionId,
child,
]),
);
for (const [sessionId, items] of backgroundLists(session, rootSessionId)) {
const json = JSON.stringify(items);
if (this.published.get(sessionId)?.json === json) continue;
this.published.set(sessionId, { json, root: rootSessionId });
const child = children.get(sessionId);
this.sink.changed({
sessionId,
items,
state: child ? workStateName(child.workState) : rootState,
...(child ? { parentSessionId: child.parentSessionId } : {}),
});
sync(
rootSessionId: string,
session: BackgroundOwner & { background?: SessionBackgroundWork },
rootState: WorkStateName,
): void {
if (!this.sink || !session.background?.reported) return;
for (const report of session.background.reports(session, rootState)) {
const json = JSON.stringify(report.items);
if (this.published.get(report.sessionId)?.json === json) continue;
this.published.set(report.sessionId, { json, root: rootSessionId });
this.sink.changed(report);
}
}

12
src/binary-entry.ts Normal file
View File

@@ -0,0 +1,12 @@
// Entry of the release executable (`scripts/build-binary.mjs`). It registers the
// Claude CLI shipped next to the executable before importing the agent:
// index.ts starts serving (or, with --cli, spawns the CLI) while it evaluates,
// and a static import would evaluate it before this module's body runs.
import { realpathSync } from "node:fs";
import { dirname } from "node:path";
import { useBundledClaudeCli } from "./claude-cli.js";
// realpath, so a symlink to the executable (e.g. in /usr/local/bin) still finds
// the CLI next to the real file.
useBundledClaudeCli(dirname(realpathSync(process.execPath)));
await import("./index.js");

88
src/claude-cli.ts Normal file
View File

@@ -0,0 +1,88 @@
import { existsSync } from "node:fs";
import { join } from "node:path";
const CLI_FILE_NAME = process.platform === "win32" ? "claude.exe" : "claude";
let bundledCliDirectory: string | undefined;
/**
* Called by the executable's entry (`src/binary-entry.ts`) before the agent
* starts. A `bun build --compile` executable has no `node_modules` to resolve
* the SDK's platform package from, so the release archive ships the Claude CLI
* next to it. The CLI isn't embedded: a child process can't run a file inside
* Bun's `$bunfs`, so an embedded copy would have to be extracted to disk first.
*/
export function useBundledClaudeCli(directory: string): void {
bundledCliDirectory = directory;
}
/**
* The Claude CLI binary to spawn: `CLAUDE_CODE_EXECUTABLE` if set, then the CLI
* shipped next to the executable, then the SDK's platform-specific package.
*/
export async function claudeCliPath(): Promise<string> {
if (process.env.CLAUDE_CODE_EXECUTABLE) {
return process.env.CLAUDE_CODE_EXECUTABLE;
}
if (bundledCliDirectory !== undefined) {
return bundledClaudeCliPath(bundledCliDirectory);
}
return installedClaudeCliPath();
}
function bundledClaudeCliPath(directory: string): string {
const path = join(directory, CLI_FILE_NAME);
// Without this check, spawning a missing CLI fails with a bare ENOENT.
if (!existsSync(path)) {
throw new Error(
`The Claude CLI is missing: expected ${path}. Keep ${CLI_FILE_NAME} from the release archive ` +
"in the same directory as claude-agent-acp, or set CLAUDE_CODE_EXECUTABLE to a Claude CLI binary.",
);
}
return path;
}
async function installedClaudeCliPath(): Promise<string> {
// The SDK's CLI is a native binary shipped as a platform-specific optional
// dependency of @anthropic-ai/claude-agent-sdk. Resolve via a require bound
// to the SDK so nested installs are found even when npm doesn't hoist.
const { createRequire } = await import("node:module");
const req = createRequire(import.meta.resolve("@anthropic-ai/claude-agent-sdk"));
const ext = process.platform === "win32" ? ".exe" : "";
// On linux, both glibc and musl variants may be installed side-by-side
// (e.g. bunx hydrates every optional dep), so picking one by trial is
// unreliable: the wrong binary segfaults at runtime instead of failing to
// spawn. Detect the runtime libc and prefer the matching variant, falling
// back to the other only if the preferred one isn't installed.
const candidates =
process.platform === "linux"
? isMuslLibc()
? [
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}-musl/claude${ext}`,
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}/claude${ext}`,
]
: [
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}/claude${ext}`,
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}-musl/claude${ext}`,
]
: [`@anthropic-ai/claude-agent-sdk-${process.platform}-${process.arch}/claude${ext}`];
for (const candidate of candidates) {
try {
return req.resolve(candidate);
} catch {
// try next candidate
}
}
throw new Error(
`Claude native binary not found for ${process.platform}-${process.arch}. ` +
`Reinstall @anthropic-ai/claude-agent-sdk without --omit=optional, or set CLAUDE_CODE_EXECUTABLE.`,
);
}
function isMuslLibc(): boolean {
// process.report.getReport().header.glibcVersionRuntime is populated when
// Node is dynamically linked against glibc, and absent on musl.
const report = process.report?.getReport() as
{ header?: { glibcVersionRuntime?: string } } | undefined;
return !report?.header?.glibcVersionRuntime;
}

View File

@@ -2,7 +2,7 @@ import { AIR_CONTEXT_COMPACTION_KEY, withAirMeta } from "./air-extension.js";
export const CONTEXT_COMPACTION_META_VERSION = 1;
export type ContextCompactionTrigger = "manual" | "automatic";
export type ContextCompactionTrigger = "manual" | "automatic" | "idle";
export interface ContextCompactionMetadata {
version: typeof CONTEXT_COMPACTION_META_VERSION;

View File

@@ -1,6 +1,7 @@
import { ClientCapabilities, SessionNotification } from "@agentclientprotocol/sdk/unstable";
import {
ContextCompactionMetadata,
ContextCompactionTrigger,
createContextCompactionMeta,
} from "./context-compaction-meta.js";
import { compactionToolCall } from "./tool-calls/reporters/compaction.js";
@@ -66,7 +67,7 @@ const ANALYSIS_BLOCKS = [
* it as the post-compaction user message. */
const CONTINUATION_PREAMBLE = /^\s*This session is being continued from a previous conversation/i;
const PERSISTED_PREAMBLE =
/^\s*This session is being continued from a previous conversation[^\n]*\n+Summary:[ \t]*\r?\n/;
/^\s*This session is being continued from a previous conversation[^\n]*\n(?:[\s\S]*?\n)?Summary:[ \t]*\r?\n/;
const PERSISTED_TRAILERS = [
/\n+If you need specific details from before compaction[\s\S]*$/,
/\n+(?:Please )?[Cc]ontinue the conversation from where it left (?:it )?off[\s\S]*$/,
@@ -408,14 +409,29 @@ function withoutError({
return facts;
}
export function contextCompactionMetadataFromBoundary(compactMetadata: {
trigger: "manual" | "auto";
pre_tokens: number;
post_tokens?: number;
duration_ms?: number;
}): Omit<ContextCompactionMetadata, "version"> {
function compactionTrigger(
cliTrigger: "manual" | "auto",
sentByAdapter: boolean,
): ContextCompactionTrigger {
// The CLI's own automatic compaction stays automatic, even inside the adapter's /compact turn.
if (cliTrigger === "auto") return "automatic";
// The CLI reports every /compact as manual; only the adapter knows it sent this one itself.
if (sentByAdapter) return "idle";
return "manual";
}
/** `idleCompaction` marks a `/compact` that the adapter sent itself (see `IdleCompaction`). */
export function contextCompactionMetadataFromBoundary(
compactMetadata: {
trigger: "manual" | "auto";
pre_tokens: number;
post_tokens?: number;
duration_ms?: number;
},
{ idleCompaction = false }: { idleCompaction?: boolean } = {},
): Omit<ContextCompactionMetadata, "version"> {
return {
trigger: compactMetadata.trigger === "auto" ? "automatic" : "manual",
trigger: compactionTrigger(compactMetadata.trigger, idleCompaction),
preTokens: compactMetadata.pre_tokens,
...(compactMetadata.post_tokens !== undefined
? { postTokens: compactMetadata.post_tokens }

View File

@@ -303,7 +303,7 @@ export function applyAskElicitationResponse(
questions: AskUserQuestion[],
options: { reportUnanswered?: boolean } = {},
): AskUserQuestionOutcome {
// Typed against the output schema: as of SDK 0.3.316 only it lists
// Typed against the output schema: as of SDK 0.3.319 only it lists
// `unanswered`, though the tool reads the same annotations from its input.
const annotations: NonNullable<AskUserQuestionOutput["annotations"]> = {};
const withAnnotations = (answers: AskUserQuestionOutput["answers"]) => ({

240
src/idle-compaction.ts Normal file
View File

@@ -0,0 +1,240 @@
export type PromptCacheTtl = "5m" | "1h";
export type IdleCompactionHost = {
/** Claude is working (`startTurn`'s check) or a permission request / user input is pending. */
isBusy(): boolean;
/** Context size after the last main-thread request, or undefined if unknown. */
contextTokens(): number | undefined;
/** Current merged Claude Code settings. */
settings(): {
idleCompaction?: boolean;
autoCompactEnabled?: boolean;
env?: Record<string, string | undefined>;
};
/** Runs `/compact` as an adapter-originated turn; settles with that turn. */
compact(): Promise<unknown>;
/** Debug-level log. */
log(message: string): void;
};
export type IdleCompactionOptions = {
env?: Record<string, string | undefined>;
setTimeout?: (callback: () => void, ms: number) => unknown;
};
/** The longest delay `setTimeout` takes; a longer one fires at once. */
const MAX_TIMER_MS = 2_147_483_647;
const HOUR_MS = 3_600_000;
const DEFAULT_MIN_TOKENS = 200_000;
const MIN_TOKENS_FLOOR = 100_000;
// 54 min for a 1h cache: leaves a margin for the gap between the provider
// writing the cache and the adapter receiving the response.
const DEFAULT_DELAY_MS = 0.9 * HOUR_MS;
// Node timers do not run while the machine sleeps, so one can fire hours after
// its deadline, on a cache that expired meanwhile. Same threshold as Claude Code.
const LAPSED_AFTER_MS = 60_000;
// The values Claude Code reads as "on" for env flags such as DISABLE_COMPACT
// (case-insensitive, trimmed).
const TRUTHY_ENV = new Set(["1", "true", "yes", "on"]);
function isTruthyEnv(value: string | undefined): boolean {
return value !== undefined && TRUTHY_ENV.has(value.trim().toLowerCase());
}
function parseCacheTtl(usage: unknown): PromptCacheTtl | undefined {
const creation = (usage as { cache_creation?: unknown } | null | undefined)?.cache_creation;
if (typeof creation !== "object" || creation === null) return undefined;
const { ephemeral_1h_input_tokens: hour, ephemeral_5m_input_tokens: fiveMin } = creation as {
ephemeral_1h_input_tokens?: unknown;
ephemeral_5m_input_tokens?: unknown;
};
if (typeof hour === "number" && hour > 0) return "1h";
if (typeof fiveMin === "number" && fiveMin > 0) return "5m";
return undefined;
}
/**
* Compacts a session shortly before its one-hour prompt cache expires, so the
* next prompt writes a short summary instead of the whole context. Claude Code
* has this feature but arms it only for "attended" sessions, which SDK sessions
* are not. One instance per session.
*/
export class IdleCompaction {
private readonly env: Record<string, string | undefined>;
private readonly setTimer: (callback: () => void, ms: number) => unknown;
private ttl: PromptCacheTtl | undefined;
private lastRequestAt: number | undefined;
private lastMessageId: string | undefined;
/** Set when `/compact` was sent, cleared by the next request: a compaction that failed,
* or left the context large, would otherwise re-arm at once and loop until the user prompts. */
private compactedSinceLastRequest = false;
private timer: unknown;
private compacting = false;
private disposed = false;
private warnedInvalidDelay = false;
/** `status` of the last `rate_limit_event`; unknown until one arrives. */
private rateLimitStatus: string | undefined;
constructor(
private readonly host: IdleCompactionHost,
options: IdleCompactionOptions = {},
) {
this.env = options.env ?? process.env;
this.setTimer = options.setTimeout ?? ((cb, ms) => setTimeout(cb, ms));
}
/** Whether an idle compaction it started is still running. `acp-agent.ts` reports
* `trigger: "idle"` while it is true. A client's `/compact` queued behind is read
* only after the adapter's turn's `result`, which clears `compacting`; this relies
* on the CLI emitting that compaction's `compact_boundary` after the previous
* result, which nothing here enforces. */
get isCompacting(): boolean {
return this.compacting;
}
/** A main-thread response arrived. Records its time and cache TTL, and drops any armed timer.
* The provider writes the cache when the request starts, so the deadline is measured from the
* response's first sighting: the `message_start` stream event, or its first `assistant` message
* when it was not streamed. The CLI sends one `assistant` message per content block, and a
* 7-minute response would otherwise put the deadline 7 minutes after the cache was written.
* A later call with the same `messageId` only updates the TTL. */
noteRequest(usage: unknown, messageId?: string): void {
if (this.disposed) return;
this.ttl = parseCacheTtl(usage) ?? this.ttl;
if (messageId !== undefined && messageId === this.lastMessageId) return;
this.lastMessageId = messageId;
this.lastRequestAt = Date.now();
this.compactedSinceLastRequest = false;
this.disarm("new request");
}
/** The session became idle. Arms the timer if every condition holds, else logs why not. */
onIdle(): void {
if (this.disposed) return;
const skip = this.skipReason();
if (skip) {
this.host.log(`[idle-compaction] skip: ${skip}`);
return;
}
this.disarm("rearm");
const deadline = this.lastRequestAt! + this.delayMs();
this.timer = this.setTimer(() => void this.fire(), Math.max(0, deadline - Date.now()));
(this.timer as { unref?: () => void } | undefined)?.unref?.();
this.host.log(`[idle-compaction] armed deadline=${new Date(deadline).toISOString()}`);
}
/** A `rate_limit_event` arrived (`SDKRateLimitInfo`). Anything but `allowed` skips compaction,
* as in Claude Code. This is account state, so `reset()` keeps it. */
noteRateLimit(info: unknown): void {
const status = (info as { status?: unknown } | null | undefined)?.status;
if (typeof status === "string") this.rateLimitStatus = status;
}
/** Claude Code reported its compaction as failed. Its turn still ends normally, so `compact()` does not reject. */
noteCompactionFailed(error: string | undefined): void {
if (!this.compacting) return;
this.host.log(`[idle-compaction] compaction failed: ${error ?? "no reason given"}`);
}
/** The conversation was replaced (`/clear`): forgets the recorded request and TTL, and drops any armed timer. */
reset(): void {
this.ttl = undefined;
this.lastRequestAt = undefined;
this.lastMessageId = undefined;
this.compactedSinceLastRequest = false;
this.disarm("conversation reset");
}
/** Drops the armed timer, if any. `reason` is only logged. */
disarm(reason: string): void {
if (this.timer === undefined) return;
clearTimeout(this.timer as ReturnType<typeof setTimeout>);
this.timer = undefined;
this.host.log(`[idle-compaction] disarmed: ${reason}`);
}
/** Disarms and ignores every later call. */
dispose(): void {
this.disarm("disposed");
this.disposed = true;
}
private async fire(): Promise<void> {
this.timer = undefined;
if (this.disposed) return;
const skip = this.skipReason();
if (skip) {
this.host.log(`[idle-compaction] skip at fire: ${skip}`);
return;
}
this.host.log("[idle-compaction] firing /compact");
this.compacting = true;
this.compactedSinceLastRequest = true;
try {
await this.host.compact();
} catch (error) {
this.host.log(`[idle-compaction] compaction failed: ${error}`);
} finally {
this.compacting = false;
}
}
/** Why no compaction should be scheduled right now, or undefined if all conditions hold. */
private skipReason(): string | undefined {
if (this.compacting) return "compaction already running";
const disabled = this.disabledBy();
if (disabled) return `disabled by ${disabled}`;
if (this.lastRequestAt === undefined) return "no request recorded in this process";
if (Date.now() > this.lastRequestAt + this.delayMs() + LAPSED_AFTER_MS) {
return "cache lapsed: more than 60 s past the deadline";
}
if (this.rateLimitStatus !== undefined && this.rateLimitStatus !== "allowed") {
return `rate limit status is ${this.rateLimitStatus}`;
}
if (this.compactedSinceLastRequest) return "already compacted for this request";
if (this.ttl !== "1h") return `cache TTL is ${this.ttl ?? "unknown"}, not 1h`;
const tokens = this.host.contextTokens();
const min = this.minTokens();
if (tokens === undefined || tokens < min) {
return `context ${tokens ?? "unknown"} tokens is below the ${min} minimum`;
}
if (this.host.isBusy()) return "session is busy";
return undefined;
}
private disabledBy(): string | undefined {
const settings = this.host.settings();
if (settings.idleCompaction === false) return "idleCompaction setting";
if (settings.autoCompactEnabled === false) return "autoCompactEnabled setting";
for (const name of ["DISABLE_COMPACT", "DISABLE_AUTO_COMPACT"]) {
if (isTruthyEnv(this.claudeCodeEnv(name))) return name;
}
return undefined;
}
/** The CLI applies the settings `env` block after the launch env, so it wins. */
private claudeCodeEnv(name: string): string | undefined {
return this.host.settings().env?.[name] ?? this.env[name];
}
/** `0` and invalid values fall back to 200,000, as in the CLI, which declares the variable
* with `min: 1` and computes `Math.max(100000, env ?? 200000)`. */
private minTokens(): number {
const raw = this.claudeCodeEnv("CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS");
const parsed = raw !== undefined && /^\d+$/.test(raw.trim()) ? Number(raw.trim()) : 0;
return Math.max(MIN_TOKENS_FLOOR, parsed > 0 ? parsed : DEFAULT_MIN_TOKENS);
}
private delayMs(): number {
const raw = this.env.CLAUDE_AGENT_ACP_IDLE_COMPACT_DELAY_MS;
if (raw === undefined) return DEFAULT_DELAY_MS;
if (/^\d+$/.test(raw.trim()) && Number(raw.trim()) <= MAX_TIMER_MS) return Number(raw.trim());
if (!this.warnedInvalidDelay) {
this.warnedInvalidDelay = true;
this.host.log(
`[idle-compaction] ignoring invalid CLAUDE_AGENT_ACP_IDLE_COMPACT_DELAY_MS=${raw}`,
);
}
return DEFAULT_DELAY_MS;
}
}

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
import { claudeCliPath } from "./acp-agent.js";
import { claudeCliPath } from "./claude-cli.js";
import { applyManagedPolicyEnv } from "./managed-policy.js";
import { acpProtocolsFromEnv, serveAcp } from "./serve.js";
import packageJson from "../package.json" with { type: "json" };

View File

@@ -111,8 +111,11 @@ function needsReconnect(status: McpServerStatus): boolean {
function codeSpan(text: string): string {
const longestRun = Math.max(0, ...(text.match(/`+/g) ?? []).map((run) => run.length));
const fence = "`".repeat(longestRun + 1);
// CommonMark strips one space from each end of a span, so a pad keeps the text unchanged.
const pad = text.startsWith("`") || text.endsWith("`") || /^ .*[^ ].* $/.test(text) ? " " : "";
// CommonMark strips one space from each end of a span that is not all spaces
// (line endings count as spaces), so a pad keeps the text unchanged. Plain
// string checks, not one regex: `^ .*[^ ].* $` backtracks quadratically.
const strippedByCommonMark = text.startsWith(" ") && text.endsWith(" ") && /[^ \r\n]/.test(text);
const pad = text.startsWith("`") || text.endsWith("`") || strippedByCommonMark ? " " : "";
return `${fence}${pad}${text}${pad}${fence}`;
}

View File

@@ -1,4 +1,4 @@
import type { PermissionMode } from "@anthropic-ai/claude-agent-sdk";
import type { PermissionMode, Settings } from "@anthropic-ai/claude-agent-sdk";
interface PermissionModeLogger {
error: (...args: unknown[]) => void;
@@ -58,3 +58,10 @@ export function resolvePermissionMode(
return mapped;
}
/** Claude Code reads `disableAutoMode` both at the top level and under `permissions`. */
export function autoModeDisabled(settings: Settings): boolean {
return (
settings.disableAutoMode === "disable" || settings.permissions?.disableAutoMode === "disable"
);
}

View File

@@ -66,29 +66,50 @@ type ModeConfigSession = SessionMode & {
configOptions: SessionConfigOption[];
};
type InitializeSessionModeParams = {
query: Pick<Query, "setPermissionMode">;
requestedMode: PermissionMode;
currentModelInfo?: ModelInfo;
currentModelId: string;
/** Which optional modes the session's mode catalog offers. */
export type ModeAvailability = {
/** When false, bypassPermissions is omitted from the mode catalog. */
allowBypass: boolean;
/** When false (`disableAutoMode: "disable"` in settings), auto is omitted. */
allowAuto: boolean;
};
type InitializeSessionModeParams = ModeAvailability & {
query: Pick<Query, "setPermissionMode">;
requestedMode: PermissionMode;
/** The mode is the built-in default: no request, transcript or setting chose it. */
builtInDefault?: boolean;
currentModelInfo?: ModelInfo;
currentModelId: string;
};
/**
* The permission mode a new Claude query starts in.
*
* An explicit mode wins. A resume or a load continues in the mode of the last
* user record of the transcript. If that mode is unknown or not available now,
* for example bypass, the session starts in `defaultMode` of the settings.
* user record of the transcript. If that mode is unknown or absent from the
* session's mode catalog, use an available `defaultMode` from settings, or
* `default` when that setting is unavailable too.
*
* With no `defaultMode` set, the session starts in `auto` when the catalog
* offers it and in `default` otherwise (`builtInDefault: true`), like an
* interactive Claude Code session. Claude Code also starts organizations with
* the HIPAA configuration in `default`; the SDK does not report that
* configuration, so those sessions start in `auto` here.
*/
export function resolveInitialPermissionMode(
sources: { explicit?: PermissionMode; resumed?: string; defaultMode?: unknown },
logger: { error: (...args: unknown[]) => void },
allowBypass: boolean,
): PermissionMode {
availability: ModeAvailability,
): { mode: PermissionMode; builtInDefault: boolean } {
const { allowBypass } = availability;
const available = (mode: PermissionMode) =>
buildAvailableModes(availability).some((entry) => entry.id === mode);
if (sources.explicit !== undefined) {
return resolvePermissionMode(sources.explicit, logger, allowBypass);
return {
mode: resolvePermissionMode(sources.explicit, logger, allowBypass),
builtInDefault: false,
};
}
if (sources.resumed !== undefined) {
let rejected = false;
@@ -97,13 +118,23 @@ export function resolveInitialPermissionMode(
{ error: () => (rejected = true) },
allowBypass,
);
if (!rejected) return mode;
if (!rejected && available(mode)) {
return { mode, builtInDefault: false };
}
logger.error(
`Ignoring permission mode "${sources.resumed}" of the resumed transcript: ` +
`not available in this session.`,
);
}
return resolvePermissionMode(sources.defaultMode, logger, allowBypass);
if (sources.defaultMode === undefined) {
return { mode: available("auto") ? "auto" : "default", builtInDefault: true };
}
const configuredMode = resolvePermissionMode(sources.defaultMode, logger, allowBypass);
// A rejected transcript mode falls back only to a mode this session can offer.
if (sources.resumed !== undefined && !available(configuredMode)) {
return { mode: "default", builtInDefault: false };
}
return { mode: configuredMode, builtInDefault: false };
}
/** Owns session-mode policy and the ACP/SDK synchronization it requires. */
@@ -113,22 +144,40 @@ export class SessionModeManager<S extends SessionMode> {
async initialize({
query,
requestedMode,
builtInDefault = false,
currentModelInfo,
currentModelId,
allowBypass,
allowAuto,
}: InitializeSessionModeParams): Promise<{
modes: SessionModeState;
autoModeFallbackWarningPending: boolean;
}> {
const availableModes = this.buildAvailableModes(allowBypass);
const availableModes = buildAvailableModes(
{ allowBypass, allowAuto },
this.options.airClient?.() ?? false,
);
let effectiveMode = requestedMode;
let autoModeFallbackWarningPending = false;
if (
const autoUnsupported =
effectiveMode === "auto" &&
currentModelInfo !== undefined &&
currentModelInfo.supportsAutoMode !== true
) {
currentModelInfo.supportsAutoMode !== true;
if (!this.isAvailable(availableModes, effectiveMode)) {
this.options.logError(
`permissions.defaultMode "${effectiveMode}" is not available in ` +
`this session; falling back to "default".`,
);
effectiveMode = "default";
await this.trySyncMode(query, effectiveMode, "Failed to sync clamped permissionMode to SDK:");
} else if (autoUnsupported && builtInDefault) {
// Like Claude Code: a built-in Auto default the model can't serve starts
// in Manual, without a warning. Nobody chose Auto, so don't escalate to
// acceptEdits.
effectiveMode = "default";
await this.trySyncMode(query, effectiveMode, "Failed to sync clamped permissionMode to SDK:");
} else if (autoUnsupported) {
this.options.logError(
`permissions.defaultMode "auto" is not available for model ` +
`"${currentModelId}"; falling back to "${AUTO_MODE_FALLBACK}".`,
@@ -136,13 +185,6 @@ export class SessionModeManager<S extends SessionMode> {
effectiveMode = AUTO_MODE_FALLBACK;
autoModeFallbackWarningPending = true;
await this.trySyncMode(query, effectiveMode, "Failed to sync clamped permissionMode to SDK:");
} else if (!this.isAvailable(availableModes, effectiveMode)) {
this.options.logError(
`permissions.defaultMode "${effectiveMode}" is not available in ` +
`this session; falling back to "default".`,
);
effectiveMode = "default";
await this.trySyncMode(query, effectiveMode, "Failed to sync clamped permissionMode to SDK:");
}
return {
@@ -350,50 +392,6 @@ export class SessionModeManager<S extends SessionMode> {
}
}
private buildAvailableModes(allowBypass: boolean): SessionModeState["availableModes"] {
// Only AIR gets the mode kind.
const air = this.options.airClient?.() ?? false;
const kind = (value: string) => {
const meta = airOnlyMeta(air, AIR_KIND_KEY, value);
return meta ? { _meta: meta } : {};
};
const modes: SessionModeState["availableModes"] = [
{
id: "default",
name: "Manual",
description: "Always ask before making changes",
...kind("standard"),
},
{
id: "acceptEdits",
name: "Accept edits",
description: "Automatically accept all file edits",
...kind("standard"),
},
{
id: "plan",
name: "Plan",
description: "Create a plan before making changes",
...kind("plan"),
},
{
id: "auto",
name: "Auto",
description: "Claude handles permission decisions",
...kind("auto_review"),
},
];
if (allowBypass) {
modes.push({
id: "bypassPermissions",
name: "Bypass permissions",
description: "Accepts all permissions",
...kind("full_access"),
});
}
return modes;
}
private async trySyncMode(
query: Pick<Query, "setPermissionMode">,
mode: PermissionMode,
@@ -406,3 +404,51 @@ export class SessionModeManager<S extends SessionMode> {
}
}
}
function buildAvailableModes(
{ allowBypass, allowAuto }: ModeAvailability,
air = false,
): SessionModeState["availableModes"] {
// Only AIR gets the mode kind.
const kind = (value: string) => {
const meta = airOnlyMeta(air, AIR_KIND_KEY, value);
return meta ? { _meta: meta } : {};
};
const modes: SessionModeState["availableModes"] = [
{
id: "default",
name: "Manual",
description: "Always ask before making changes",
...kind("standard"),
},
{
id: "acceptEdits",
name: "Accept edits",
description: "Automatically accept all file edits",
...kind("standard"),
},
{
id: "plan",
name: "Plan",
description: "Create a plan before making changes",
...kind("plan"),
},
];
if (allowAuto) {
modes.push({
id: "auto",
name: "Auto",
description: "Claude handles permission decisions",
...kind("auto_review"),
});
}
if (allowBypass) {
modes.push({
id: "bypassPermissions",
name: "Bypass permissions",
description: "Accepts all permissions",
...kind("full_access"),
});
}
return modes;
}

View File

@@ -50,11 +50,35 @@ export function noticeOrTranscriptUpdate(
if (supportsNotices) return noticeUpdate(notice);
return {
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: transcriptText },
content: { type: "text", text: transcriptBlock(transcriptText) },
...(transcriptMeta ? { _meta: transcriptMeta } : {}),
};
}
/** Clients render consecutive agent_message_chunks as one markdown stream, so
* a bare `**Notice:** …` chunk lands mid-sentence: "…all done.**Notice:** Stop
* says: …". Put it in its own blockquote paragraph, blank lines on both sides,
* so it starts on a new line and Claude's next chunk does not continue the
* quote. */
export function transcriptBlock(text: string): string {
const lines = text.trim().split(/\r?\n/);
return `\n\n${lines.map((line) => (line ? `> ${line}` : ">")).join("\n")}\n\n`;
}
const HOOK_SPEAKER = /^[^\n:]{1,80} says: /;
/** Claude Code prefixes every line of a hook's `systemMessage` with
* "<HookEvent> says: ", so a two-line message reads "Stop says: a\nStop says:
* b". Keep the prefix on the first line only when every line repeats it. */
export function collapseHookSpeaker(text: string): string {
const lines = text.split(/\r?\n/);
const prefix = lines[0].match(HOOK_SPEAKER)?.[0];
if (!prefix || lines.length < 2 || !lines.every((line) => line.startsWith(prefix))) {
return text;
}
return [lines[0], ...lines.slice(1).map((line) => line.slice(prefix.length))].join("\n");
}
/** Fragments that read on after a bold label stand alone as a notice
* description, so start them with a capital. */
export function sentenceCase(text: string): string {

View File

@@ -25,7 +25,7 @@ export type UltracodeSettingsReport = {
};
// `Query.getSettings()` exists at runtime (sdk.mjs) but is missing from the
// SDK's type declarations as of 0.3.316.
// SDK's type declarations as of 0.3.319.
type GetSettingsQuery = Query & {
getSettings?: () => Promise<{
applied?: { ultracodeAvailable?: unknown; ultracodeRequested?: unknown };

View File

@@ -42,6 +42,7 @@ describe("ClaudeAcpAgent settings", () => {
value: "claude-sonnet-4-6",
displayName: "Claude Sonnet 4.5",
description: "Default",
supportsAutoMode: true,
},
],
}),
@@ -284,7 +285,7 @@ describe("ClaudeAcpAgent settings", () => {
it("drops escalating defaultMode when it comes from project-tier settings", async () => {
// bypassPermissions in .claude/settings.json (a repo-committed tier) is
// filtered by the SDK's trust policy and clamps to 'default'.
// filtered by the SDK's trust policy, so the built-in default applies.
const projectDir = path.join(tempDir, "project");
await fs.promises.mkdir(path.join(projectDir, ".claude"), { recursive: true });
await fs.promises.writeFile(
@@ -303,8 +304,8 @@ describe("ClaudeAcpAgent settings", () => {
_meta: { disableBuiltInTools: true },
});
expect(getCapturedOptions().permissionMode).toBe("default");
expect(response.modes.currentModeId).toBe("default");
expect(getCapturedOptions().permissionMode).toBe("auto");
expect(response.modes.currentModeId).toBe("auto");
});
it("honors the host opt-out from bypassPermissions", async () => {
@@ -380,7 +381,7 @@ describe("ClaudeAcpAgent settings", () => {
},
);
it("defaults to 'default' when no permissions.defaultMode is set", async () => {
it("defaults to 'auto' when no permissions.defaultMode is set", async () => {
const projectDir = path.join(tempDir, "project");
await fs.promises.mkdir(projectDir, { recursive: true });
@@ -395,8 +396,37 @@ describe("ClaudeAcpAgent settings", () => {
_meta: { disableBuiltInTools: true },
});
expect(getCapturedOptions().permissionMode).toBe("default");
expect(getCapturedOptions().permissionMode).toBe("auto");
expect(response.modes.currentModeId).toBe("auto");
});
it.each([
{ name: "top-level disableAutoMode", settings: { disableAutoMode: "disable" } },
{
name: "permissions.disableAutoMode with defaultMode 'auto'",
settings: { permissions: { disableAutoMode: "disable", defaultMode: "auto" } },
},
])("starts in 'default' without Auto in the catalog for $name", async ({ settings }) => {
await fs.promises.writeFile(path.join(tempDir, "settings.json"), JSON.stringify(settings));
const projectDir = path.join(tempDir, "project");
await fs.promises.mkdir(projectDir, { recursive: true });
mockQuery();
const { ClaudeAcpAgent } = await import("../acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
(agent as any).logger = { log: () => {}, error: () => {} };
const response = await (agent as any).createSession({
cwd: projectDir,
mcpServers: [],
_meta: { disableBuiltInTools: true },
});
expect(response.modes.currentModeId).toBe("default");
expect(response.modes.availableModes.map((mode: { id: string }) => mode.id)).not.toContain(
"auto",
);
});
describe("on resume", () => {
@@ -464,6 +494,16 @@ describe("ClaudeAcpAgent settings", () => {
expect(response.modes.currentModeId).toBe("plan");
});
it("uses the available configured mode when the restored mode is not in the catalog", async () => {
const { permissionMode, response } = await resume([user("dontAsk")]);
expect(permissionMode).toBe("plan");
expect(response.modes.currentModeId).toBe("plan");
expect(response.configOptions.find((option: any) => option.id === "mode").currentValue).toBe(
"plan",
);
});
it("uses permissions.defaultMode when bypass is not available now", async () => {
const { permissionMode, response } = await resume([user("bypassPermissions")], {
claudeCode: { options: { allowDangerouslySkipPermissions: false } },
@@ -536,7 +576,7 @@ describe("ClaudeAcpAgent settings", () => {
});
});
it("falls back to 'default' when permissions.defaultMode is invalid", async () => {
it("falls back to the built-in default when permissions.defaultMode is invalid", async () => {
await fs.promises.writeFile(
path.join(tempDir, "settings.json"),
JSON.stringify({
@@ -560,9 +600,9 @@ describe("ClaudeAcpAgent settings", () => {
_meta: { disableBuiltInTools: true },
});
// Bad mode is ignored at the usage site; session creation must not throw.
expect(getCapturedOptions().permissionMode).toBe("default");
expect(response.modes.currentModeId).toBe("default");
// Settings resolution drops the bad mode; session creation must not throw.
expect(getCapturedOptions().permissionMode).toBe("auto");
expect(response.modes.currentModeId).toBe("auto");
});
it("ignores model from settings when it is not a string", async () => {
@@ -704,6 +744,30 @@ describe("ClaudeAcpAgent settings", () => {
expect(modeIds).not.toContain("dontAsk");
});
it("starts the built-in Auto default in Manual on an unsupported model", async () => {
const projectDir = path.join(tempDir, "project");
await fs.promises.mkdir(projectDir, { recursive: true });
const { setPermissionModeSpy } = mockQueryWithModels([
{ value: "claude-haiku-4-5", displayName: "Claude Haiku", description: "Fast" },
]);
const { ClaudeAcpAgent } = await import("../acp-agent.js");
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent(createMockClient());
const response = await (agent as any).createSession({
cwd: projectDir,
mcpServers: [],
_meta: { disableBuiltInTools: true },
});
expect(setPermissionModeSpy).toHaveBeenCalledWith("default");
expect(response.modes.currentModeId).toBe("default");
expect((agent as any).sessions[response.sessionId].autoModeFallbackWarningPending).toBe(
false,
);
});
it("falls back permissions.defaultMode='auto' to Accept edits on an unsupported model", async () => {
await fs.promises.writeFile(
path.join(tempDir, "settings.json"),

View File

@@ -42,7 +42,6 @@ import {
isSyntheticLoginMessage,
stripLocalCommandMetadata,
ClaudeAcpAgent,
claudeCliPath,
computeSessionFingerprint,
streamEventToAcpNotifications,
messageIdForGrouping,
@@ -56,6 +55,7 @@ import {
type StreamedToolInputCache,
} from "../acp-agent.js";
import { SessionTitles } from "../session-titles.js";
import { SessionBackgroundWork } from "../background-work.js";
import { formatUsageResponse, isUsageCommandText, parseUsageResponse } from "../usage-markdown.js";
import { Pushable } from "../utils.js";
import { heldRequest, initializeClient, makeMockQuery } from "./helpers.js";
@@ -4935,11 +4935,6 @@ describe("prompt conversion", () => {
});
describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("SDK behavior", () => {
it("finds vendored cli path", async () => {
const path = await claudeCliPath();
expect(path).toMatch(/@anthropic-ai\/claude-agent-sdk-[^/]+\/claude(\.exe)?$/);
});
it("query has a 'default' model", async () => {
const q = query({ prompt: "hi" });
const models = await q.supportedModels();
@@ -5201,6 +5196,7 @@ describe("permission request cancellation", () => {
toolUseCache: {},
emittedToolCalls: new Set(),
liveBackgroundTasks: new Map(),
background: new SessionBackgroundWork(sessionId, false),
emittedAssistantText: false,
owedTrailingIdles: 0,
messageIdToUuid: new Map(),
@@ -13862,7 +13858,7 @@ describe("MCP server startup failures", () => {
);
expect(lines).toHaveLength(1);
expect(lines[0].content.text).toBe(
'**MCP server "client-fs" failed to start:** Its tools are unavailable in this session. Run /mcp for the error.',
'\n\n> **MCP server "client-fs" failed to start:** Its tools are unavailable in this session. Run /mcp for the error.\n\n',
);
});
});
@@ -14998,6 +14994,7 @@ describe("session/close", () => {
toolUseCache: {},
emittedToolCalls: new Set(),
liveBackgroundTasks: new Map(),
background: new SessionBackgroundWork(sessionId, false),
emittedAssistantText: false,
owedTrailingIdles: 0,
messageIdToUuid: new Map(),
@@ -15120,6 +15117,7 @@ describe("session/delete", () => {
toolUseCache: {},
emittedToolCalls: new Set(),
liveBackgroundTasks: new Map(),
background: new SessionBackgroundWork(sessionId, false),
emittedAssistantText: false,
owedTrailingIdles: 0,
messageIdToUuid: new Map(),
@@ -15283,6 +15281,7 @@ describe("getOrCreateSession param change detection", () => {
toolUseCache: {},
emittedToolCalls: new Set(),
liveBackgroundTasks: new Map(),
background: new SessionBackgroundWork(sessionId, false),
emittedAssistantText: false,
owedTrailingIdles: 0,
messageIdToUuid: new Map(),
@@ -18611,8 +18610,8 @@ describe("assembled assistant text fallback", () => {
]);
expect(child.some((update) => update.sessionUpdate === "compaction_update")).toBe(false);
// Unrecognized framing: the text stays visible rather than vanishing
// behind a summary-less entity.
// Unrecognized framing: still the compaction isCompactSummary says it is,
// without summary text, and never a message from the user.
const unknownFraming = await replay(true, [
{
...summaryMessage("odd-summary"),
@@ -18623,12 +18622,14 @@ describe("assembled assistant text fallback", () => {
},
},
]);
expect(unknownFraming.some((update) => update.sessionUpdate === "compaction_update")).toBe(
false,
);
expect(unknownFraming.some((update) => update.sessionUpdate === "user_message_chunk")).toBe(
true,
);
expect(unknownFraming).toEqual([
{
sessionUpdate: "compaction_update",
compactionId: "odd-summary",
status: "completed",
_meta: { jetbrains: { air: { version: 1, contextCompaction: { version: 1 } } } },
},
]);
});
it("preserves the model response after multiple compactions in one turn", async () => {
@@ -18849,12 +18850,44 @@ describe("assembled assistant text fallback", () => {
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "1+2" }] });
const chunks = updates.filter(({ update }) => update.sessionUpdate === "agent_message_chunk");
expect(messageChunkTexts(updates)).toEqual(["**Warning:** hook says no"]);
expect(messageChunkTexts(updates)).toEqual(["\n\n> **Warning:** hook says no\n\n"]);
expect(chunks[0]?.update).toMatchObject({
_meta: { claudeCode: { kind: "informational", level: "warning" } },
});
});
it("logs session_writes_lost after the last result without sending an update", async () => {
const updates: any[] = [];
const error = vi.fn();
const agent = new ClaudeAcpAgent(
{ sessionUpdate: async (n: any) => updates.push(n) } as unknown as AcpClient,
{ log: () => {}, error },
);
injectSession(agent, [
replayedResult("done"),
idle,
{
type: "system",
subtype: "session_writes_lost",
error_code: "ENOSPC",
lost: ["transcript", "global_config"],
transcript_session_ids: ["test-session"],
uuid: randomUUID(),
session_id: "test-session",
},
]);
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "1+2" }] });
const sentAtTurnEnd = updates.length;
await vi.waitFor(() =>
expect(error).toHaveBeenCalledWith(
"Session test-session: Claude Code could not save transcript and global_config (ENOSPC); resuming may miss part of the conversation",
),
);
expect(updates).toHaveLength(sentAtTurnEnd);
});
it("marks info-level notices even when their text has no severity prefix", async () => {
const { agent, updates } = createMockAgentWithCapture();
injectSession(agent, [
@@ -18872,12 +18905,33 @@ describe("assembled assistant text fallback", () => {
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "1+2" }] });
const chunks = updates.filter(({ update }) => update.sessionUpdate === "agent_message_chunk");
expect(messageChunkTexts(updates)).toEqual(["plain hook detail"]);
expect(messageChunkTexts(updates)).toEqual(["\n\n> plain hook detail\n\n"]);
expect(chunks[0]?.update).toMatchObject({
_meta: { claudeCode: { kind: "informational", level: "info" } },
});
});
it("renders a multi-line hook notice as one quote with a single speaker prefix", async () => {
const { agent, updates } = createMockAgentWithCapture();
injectSession(agent, [
{
type: "system",
subtype: "informational",
content: "Stop says: token: 3 calls\nStop says: hand off now",
level: "notice",
session_id: "test-session",
},
result(),
idle,
]);
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "1+2" }] });
expect(messageChunkTexts(updates)).toEqual([
"\n\n> **Notice:** Stop says: token: 3 calls\n> hand off now\n\n",
]);
});
it("delivers an informational frame as a notice and still owns the result text", async () => {
const { agent, updates } = createMockAgentWithCapture();
await initializeClient(agent, { session: { notices: {} } });
@@ -19764,6 +19818,7 @@ describe("post-error recovery", () => {
toolUseCache: {},
emittedToolCalls: new Set(),
liveBackgroundTasks: new Map(),
background: new SessionBackgroundWork("test-session", false),
emittedAssistantText: false,
owedTrailingIdles: 0,
messageIdToUuid: new Map(),
@@ -25919,6 +25974,82 @@ describe("deferred settlement for live background subagents (issues #864/#866)",
]);
});
// Issue #1453: a previous cycle's trailing idle completed the next turn task.
it.each(["result", "idle"] as const)(
"keeps the next turn task open across an owed idle until its own %s",
async (end) => {
const { agent, asyncTaskUpdates } = await airAgent();
let wake = () => {};
const woken = new Promise<void>((resolve) => (wake = resolve));
let finish = () => {};
const finished = new Promise<void>((resolve) => (finish = resolve));
injectGeneratorSession(agent, (input) => {
async function* messageGenerator() {
const iter = input[Symbol.asyncIterator]();
yield userEcho((await iter.next()).value);
yield running();
yield resultMessage();
yield idle();
await woken;
yield running();
yield assistantText("first autonomous cycle");
yield resultMessage({ origin: { kind: "task-notification" } });
// The CLI stays running: the next output opens a new turn task.
yield assistantText("next autonomous cycle");
yield idle(); // The previous autonomous result's late trailer.
await finished;
yield assistantText("still working in the next cycle");
if (end === "result") {
yield resultMessage({ origin: { kind: "task-notification" } });
}
yield idle();
}
return messageGenerator();
});
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
wake();
try {
await waitFor(
() =>
asyncTaskUpdates().filter((u) => u.sessionUpdate === "async_task_spawned").length ===
2 && agent.sessions["test-session"].owedTrailingIdles === 0,
);
const [first, firstEnd, next] = asyncTaskUpdates();
expect(asyncTaskUpdates()).toEqual([
first,
{
sessionUpdate: "async_task_state_update",
asyncTaskId: first.asyncTaskId,
state: "completed",
},
next,
]);
expect(firstEnd.state).toBe("completed");
expect(next.sessionUpdate).toBe("async_task_spawned");
expect(next.asyncTaskId).not.toBe(first.asyncTaskId);
} finally {
finish();
await agent.sessions["test-session"]?.consumer;
}
const [first, , next] = asyncTaskUpdates();
expect(asyncTaskUpdates()).toEqual([
first,
{
sessionUpdate: "async_task_state_update",
asyncTaskId: first.asyncTaskId,
state: "completed",
},
next,
{
sessionUpdate: "async_task_state_update",
asyncTaskId: next.asyncTaskId,
state: "completed",
},
]);
},
);
describe("started by a scheduler fire", () => {
const lifecycleStarted = (fire: { wakeup_source?: string; scheduled_for?: number }) =>
({
@@ -28199,6 +28330,7 @@ describe("session/cancel wedge recovery (issue #680)", () => {
toolUseCache: {},
emittedToolCalls: new Set(),
liveBackgroundTasks: new Map(),
background: new SessionBackgroundWork("test-session", false),
emittedAssistantText: false,
owedTrailingIdles: 0,
messageIdToUuid: new Map(),
@@ -30107,6 +30239,7 @@ describe("agent selection config option", () => {
toolUseCache: {},
emittedToolCalls: new Set(),
liveBackgroundTasks: new Map(),
background: new SessionBackgroundWork(sessionId, false),
emittedAssistantText: false,
owedTrailingIdles: 0,
messageIdToUuid: new Map(),
@@ -30172,6 +30305,61 @@ describe("agent selection config option", () => {
});
});
describe("rate_limit_event", () => {
it("rides on the next usage_update when it comes before the turn's first assistant frame", async () => {
const updates: SessionNotification[] = [];
const agent = new ClaudeAcpAgent(
{
sessionUpdate: async (notification: SessionNotification) => {
updates.push(notification);
},
} as unknown as AcpClient,
{ log: () => {}, error: () => {} },
);
const rateLimit = { status: "allowed", resetsAt: 1700000000, rateLimitType: "seven_day" };
const usage = {
input_tokens: 10,
output_tokens: 5,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
};
injectGeneratorSession(agent, async function* (input) {
const { value } = await input[Symbol.asyncIterator]().next();
yield userEcho(value);
yield {
type: "rate_limit_event",
rate_limit_info: rateLimit,
uuid: randomUUID(),
session_id: "test-session",
};
yield {
type: "assistant",
parent_tool_use_id: null,
uuid: randomUUID(),
session_id: "test-session",
message: {
id: "msg_rate",
role: "assistant",
model: "claude-sonnet-4-20250514",
content: [{ type: "text", text: "ok" }],
usage,
},
};
yield successfulResultMessage({ usage });
yield { type: "system", subtype: "session_state_changed", state: "idle" };
});
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
const usageMetas = updates
.map((notification) => notification.update)
.filter((update) => update.sessionUpdate === "usage_update")
.map((update) => update._meta?.["_claude/rateLimit"]);
expect(usageMetas[0]).toEqual(rateLimit);
expect(usageMetas.slice(1)).toEqual(usageMetas.slice(1).map(() => undefined));
});
});
describe("the SDK session id of a fresh Claude context", () => {
// After a clear-context restart the SDK runs under a new session id, and
// every SDK message carries it. The client knows only the ACP session id.

View File

@@ -12,6 +12,6 @@
{"kind":"sessionUpdate","payload":{"sessionId":"<session>:replay-subagent:toolu_r_task","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_r_task"}},"content":{"text":"Subagent text.","type":"text"},"messageId":"msg_r_sub","sessionUpdate":"agent_message_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"content":{"text":"Fixed.","type":"text"},"messageId":"msg_r2","sessionUpdate":"agent_message_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"sessionUpdate":"subagent_state_update","state":"completed","subagentSessionId":"<session>:replay-subagent:toolu_r_task"}}}
{"kind":"loadSession","payload":{"configOptions":[{"category":"mode","currentValue":"default","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":"default"},"sessionId":"<session>"}}
{"kind":"loadSession","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>:replay-subagent:toolu_r_task","update":{"configOptions":[{"category":"model","currentValue":"claude-sonnet-4-6","id":"model","name":"Model","options":[{"name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
{"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,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":{}},"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":"newSession","payload":{"configOptions":[{"category":"mode","currentValue":"default","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":"default"},"sessionId":"<session>"}}
{"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,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -19,4 +19,4 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"description":"The history of 1 subagent of this session could not be restored.","sessionUpdate":"notice","severity":"info","title":"Some subagent history is not available"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"session_info_update","title":"Fix the app","updatedAt":"2026-01-01T00:00:00.000Z"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"idle"}}}
{"kind":"response","method":"session/resume","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":"default","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"}]}}
{"kind":"response","method":"session/resume","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"}]}}

View File

@@ -1,2 +1,2 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -1,5 +1,5 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"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":"default","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":"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>"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"background":[],"sessionUpdate":"state_update","state":"running"}}}

View File

@@ -9,5 +9,5 @@
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolName":"Edit"}},"rawOutput":"The file <cwd>/src/app.ts has been updated successfully.","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_r_edit"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolName":"Task"}},"content":[{"content":{"text":"Report.","type":"text"},"type":"content"}],"rawOutput":"Report.","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_r_task"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"content":{"text":"Fixed.","type":"text"},"messageId":"msg_r2","sessionUpdate":"agent_message_chunk"}}}
{"kind":"loadSession","payload":{"configOptions":[{"category":"mode","currentValue":"default","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"kind":"standard"},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"kind":"full_access"},"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":{"kind":"standard"},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"kind":"full_access"},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"default"},"sessionId":"<session>"}}
{"kind":"loadSession","payload":{"configOptions":[{"category":"mode","currentValue":"auto","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"kind":"standard"},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"kind":"full_access"},"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":{"kind":"standard"},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"kind":"full_access"},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"auto"},"sessionId":"<session>"}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"availableCommands":[],"sessionUpdate":"available_commands_update"}}}

View File

@@ -1,3 +1,3 @@
{"kind":"initialize","payload":{"_meta":{"goal":{"actions":["set","clear"],"controlMethod":"_session/goal","version":1},"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","recommendedValue"],"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"authMethods":[],"protocolVersion":1}}
{"kind":"newSession","payload":{"configOptions":[{"category":"mode","currentValue":"default","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"kind":"standard"},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"kind":"full_access"},"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":{"kind":"standard"},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"kind":"full_access"},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"default"},"sessionId":"<session>"}}
{"kind":"newSession","payload":{"configOptions":[{"category":"mode","currentValue":"auto","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"kind":"standard"},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"kind":"full_access"},"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":{"kind":"standard"},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"kind":"full_access"},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"auto"},"sessionId":"<session>"}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"availableCommands":[],"sessionUpdate":"available_commands_update"}}}

View File

@@ -10,5 +10,5 @@
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolName":"Edit"}},"rawOutput":"The file <cwd>/src/app.ts has been updated successfully.","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_r_edit"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolName":"Task"}},"content":[{"content":{"text":"Report.","type":"text"},"type":"content"}],"rawOutput":"Report.","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_r_task"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"content":{"text":"Fixed.","type":"text"},"messageId":"msg_r2","sessionUpdate":"agent_message_chunk"}}}
{"kind":"loadSession","payload":{"configOptions":[{"category":"mode","currentValue":"default","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"kind":"standard"},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"kind":"full_access"},"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":{"kind":"standard"},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"kind":"full_access"},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"default"},"sessionId":"<session>"}}
{"kind":"loadSession","payload":{"configOptions":[{"category":"mode","currentValue":"auto","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"kind":"standard"},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"kind":"full_access"},"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":{"kind":"standard"},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"kind":"full_access"},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"auto"},"sessionId":"<session>"}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"availableCommands":[],"sessionUpdate":"available_commands_update"}}}

View File

@@ -1,3 +1,3 @@
{"kind":"initialize","payload":{"_meta":{"goal":{"actions":["set","clear"],"controlMethod":"_session/goal","version":1},"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","recommendedValue"],"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"authMethods":[{"_meta":{"terminal-auth":{"args":["<argv>","--cli","auth","login","--claudeai"],"command":"<executable>","label":"Claude Login"}},"args":["--cli","auth","login","--claudeai"],"description":"Use Claude subscription ","id":"claude-ai-login","name":"Claude Subscription","type":"terminal"},{"_meta":{"terminal-auth":{"args":["<argv>","--cli","auth","login","--console"],"command":"<executable>","label":"Anthropic Console Login"}},"args":["--cli","auth","login","--console"],"description":"Use Anthropic Console (API usage billing)","id":"console-login","name":"Anthropic Console","type":"terminal"}],"protocolVersion":1}}
{"kind":"newSession","payload":{"configOptions":[{"category":"mode","currentValue":"default","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"kind":"standard"},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"kind":"full_access"},"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":{"kind":"standard"},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"kind":"full_access"},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"default"},"sessionId":"<session>"}}
{"kind":"newSession","payload":{"configOptions":[{"category":"mode","currentValue":"auto","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"kind":"standard"},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"kind":"full_access"},"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":{"kind":"standard"},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"kind":"standard"},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"kind":"plan"},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"kind":"auto_review"},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"kind":"full_access"},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"auto"},"sessionId":"<session>"}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"availableCommands":[],"sessionUpdate":"available_commands_update"}}}

View File

@@ -5,7 +5,7 @@
* schema, so a malformed v2 message fails the test.
*/
import { randomUUID } from "node:crypto";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { appendFile, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -615,6 +615,40 @@ describe("ACP v2 sessions", () => {
});
});
it("resumes an unavailable mode using the configured mode in the v2 response", async () => {
const project = path.join(tempDir, "claude", "projects", "-project");
await mkdir(project, { recursive: true });
await writeFile(
path.join(tempDir, "claude", "settings.json"),
JSON.stringify({ permissions: { defaultMode: "plan" } }),
);
const sessionId = randomUUID();
await writeFile(
path.join(project, `${sessionId}.jsonl`),
JSON.stringify({
type: "user",
uuid: randomUUID(),
parentUuid: null,
sessionId,
cwd,
isSidechain: false,
origin: { kind: "human" },
message: { role: "user", content: "hello" },
permissionMode: "dontAsk",
}) + "\n",
);
const { app, authUpdate } = v2Client();
await app.connectWith(connectRouter(), async (agent) => {
await initializeV2(agent);
const response = await agent.request(v2.methods.agent.session.resume, { sessionId, cwd });
expect(sdk.queryOptions.at(-1)?.permissionMode).toBe("plan");
expect(
response.configOptions?.find((option) => option.configId === "mode")?.currentValue,
).toBe("plan");
await authUpdate(1);
});
});
it("sets a config option with a v2 value and reports modes only as config options", async () => {
const client = v2Client();
const { response, updatesBeforeResponse } = await client.app.connectWith(
@@ -2695,6 +2729,78 @@ describe("ACP v2 prompts", () => {
});
});
describe("ACP v2 idle compaction", () => {
/** A main-thread request that wrote 250k tokens to the one-hour cache. */
const largeOneHourRequest = (options: Options) => ({
...assistantText(options, "Done."),
message: {
...assistantText(options, "Done.").message,
usage: {
input_tokens: 10,
output_tokens: 5,
cache_creation_input_tokens: 250_000,
cache_creation: { ephemeral_1h_input_tokens: 250_000, ephemeral_5m_input_tokens: 0 },
},
},
});
/** The session is idle for 54 minutes after a large one-hour request, and the adapter's `/compact` runs. */
it("reports the compaction that the adapter starts as running, then idle, with no user_message", async () => {
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "Date"] });
try {
const received: string[] = [];
sdk.run = async function* (prompts, options) {
const input = prompts[Symbol.asyncIterator]();
for (;;) {
const { value, done } = await input.next();
if (done) return;
received.push((value.message.content as Array<{ text: string }>)[0].text);
if (received.length === 1) {
yield {
type: "user",
message: value.message,
parent_tool_use_id: null,
uuid: value.uuid,
session_id: options.sessionId,
isReplay: true,
};
yield largeOneHourRequest(options);
} else {
yield { type: "system", subtype: "status", status: "compacting", uuid: randomUUID() };
}
yield result(options);
yield idleState(options);
}
};
const client = v2Client();
await client.app.connectWith(connectRouter(), async (agent) => {
await initializeV2(agent);
const { sessionId } = await agent.request(v2.methods.agent.session.new, { cwd });
await agent.request(v2.methods.agent.session.prompt, { sessionId, prompt: text("hi") });
await vi.advanceTimersByTimeAsync(0);
await vi.advanceTimersByTimeAsync(54 * 60_000);
for (let i = 0; i < 20 && received.length < 2; i++) await vi.advanceTimersByTimeAsync(0);
for (let i = 0; i < 20 && turnTrace(client.sessionUpdates).length < 6; i++) {
await vi.advanceTimersByTimeAsync(0);
}
await client.authUpdate(1);
});
expect(received).toEqual(["hi", "/compact"]);
expect(turnTrace(client.sessionUpdates)).toEqual([
"user_message",
"running",
"agent_message_chunk",
"idle end_turn",
"running",
"idle end_turn",
]);
} finally {
vi.useRealTimers();
}
});
});
// Claude Code runs model cycles that no prompt started, for example its
// follow-up after a background command finished. v2 reports them as
// foreground work.
@@ -4404,10 +4510,13 @@ describe("ACP v2 requests that subagents make", () => {
description: "make test",
tool_use_id: "toolu_bg",
parent_task_id: "agent-1",
owned_by_subagent: true,
is_backgrounded: true,
uuid: randomUUID(),
session_id: options.sessionId,
};
// As seen live (CLI 2.1.318): a subagent's tool result has no
// `tool_use_result`, so only the `task_started` above names the task.
yield childMessage(options, {
type: "user",
message: {
@@ -4420,13 +4529,6 @@ describe("ACP v2 requests that subagents make", () => {
},
],
},
tool_use_result: {
stdout: "",
stderr: "",
interrupted: false,
isImage: false,
backgroundTaskId: "bg1",
},
});
yield taskEnd(options, "agent-1", "toolu_agent-1");
yield level(options, [bash]);
@@ -5598,6 +5700,78 @@ describe("ACP v2 terminal of a command in the background", () => {
]);
});
it("reads the command's output file into its terminal, and the rest before its exit", async () => {
const dir = await mkdtemp(path.join(os.tmpdir(), "background-output-"));
const outputFile = path.join(dir, "tasks", "bg1.output");
await mkdir(path.dirname(outputFile));
await writeFile(outputFile, "compiling\n");
const ended = Promise.withResolvers<void>();
scriptTurns(async function* (options) {
yield level(options, [{}]);
yield taskStarted(options);
yield bashUse(options);
// The Bash result names the file, as Claude Code writes it.
const bashResult = backgrounded(options);
yield {
...bashResult,
message: {
...bashResult.message,
content: [
{
...bashResult.message.content[0],
content: `Command running in background with ID: bg1. Output is being written to: ${outputFile}`,
},
],
},
};
yield result(options);
yield idleState(options);
await ended.promise;
yield* taskEnded(options);
yield level(options, []);
});
const output = (client: ReturnType<typeof v2Client>) =>
client.sessionUpdates.flatMap(({ update }) => {
if (update.sessionUpdate === "terminal_output_chunk") {
return [`chunk ${Buffer.from(String(update.data), "base64").toString()}`];
}
if (update.sessionUpdate !== "terminal_update") return [];
return [
...(update.output
? [
`output ${Buffer.from((update.output as { data: string }).data, "base64").toString()}`,
]
: []),
...(update.exitStatus ? ["exit"] : []),
];
});
try {
const client = await promptUntil("idle end_turn [toolu_bg]", async (_, __, client) => {
await vi.waitFor(() => expect(output(client)).toContain("output compiling\n"), {
timeout: 2000,
});
await appendFile(outputFile, "tests passed\n");
await vi.waitFor(() => expect(output(client)).toContain("chunk tests passed\n"), {
timeout: 2000,
});
// The command writes its last line as it ends.
await appendFile(outputFile, "done\n");
ended.resolve();
await vi.waitFor(() => expect(backgroundTrace(client)).toContain("idle []"));
});
// The file's first text replaces the Bash result's placeholder.
expect(output(client).slice(-4)).toEqual([
"output compiling\n",
"chunk tests passed\n",
"chunk done\n",
"exit",
]);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it("ends the listed terminals when the query ends, as its background tasks end with it", async () => {
scriptTurns(async function* (options) {
yield level(options, [{}]);
@@ -5701,10 +5875,13 @@ describe("ACP v2 terminal of a command in the background", () => {
});
describe("a task that a tool call started", () => {
// Claude Code marks every live-update watcher ambient, a requested
// Monitor's task included.
const monitor = {
task_id: "mon1",
task_type: "local_monitor",
description: "Watch the logs",
ambient: true,
};
const workflow = { task_id: "wf1", task_type: "local_workflow", description: "Review" };
/** A Monitor call whose task `mon1` runs on after the call returns. The
@@ -5910,25 +6087,53 @@ describe("ACP v2 terminal of a command in the background", () => {
]);
});
it("lists a task without such a call as a _claude/task item", async () => {
it("lists a task whose call returned before the task started as a _claude/task item", async () => {
scriptTurns(async function* (options) {
yield level(options, [monitor]);
// No tool call started it, such as a task that a hook started.
yield taskEvent(options, "task_started", monitor);
yield toolUseMessage(options, "toolu_mcp", "mcp__ci__watch", { job: "build" });
yield toolResultMessage(options, "toolu_mcp", { content: "Watching the build." });
// The call's task joins the level and starts after the call returned,
// so the call ended and cannot be listed.
const task = { task_id: "mcp1", task_type: "mcp_task", description: "Watch the build" };
yield level(options, [task]);
yield taskEvent(options, "task_started", { ...task, tool_use_id: "toolu_mcp" });
yield result(options);
yield idleState(options);
});
const client = await promptUntil("idle end_turn [_claude/task]");
expect(backgroundTrace(client)).toEqual([
"running []",
"running [_claude/task]",
"idle end_turn [_claude/task]",
]);
expect(client.updates("state_update").at(-1)).toMatchObject({
background: [{ type: "_claude/task", taskId: "mon1", description: "Watch the logs" }],
background: [{ type: "_claude/task", taskId: "mcp1", description: "Watch the build" }],
});
});
// No tool call started these, such as tasks that a hook started.
it.each([
{
task: workflow,
trace: ["running []", "running [_claude/task]", "idle end_turn [_claude/task]"],
background: [{ type: "_claude/task", taskId: "wf1", description: "Review" }],
},
// A watcher Claude Code started by itself is housekeeping.
{
task: monitor,
trace: ["running []", "idle end_turn []"],
background: [],
},
])(
"lists a $task.task_type task without such a call unless it is ambient",
async ({ task, trace, background }) => {
scriptTurns(async function* (options) {
yield level(options, [task]);
yield taskEvent(options, "task_started", task);
yield result(options);
yield idleState(options);
});
const client = await promptUntil(trace.at(-1)!);
expect(backgroundTrace(client)).toEqual(trace);
expect(client.updates("state_update").at(-1)).toMatchObject({ background });
},
);
});
it("rejects terminal/cancel with -32602 when the command ends before Claude Code stops it", async () => {

View File

@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import { agentCommand } from "../agent-command.js";
describe("agentCommand", () => {
it.each([
{
runtime: "Node",
argv: ["/usr/bin/node", "/opt/acp/dist/index.js", "--hide-claude-auth"],
args: ["/opt/acp/dist/index.js", "--hide-claude-auth"],
},
{
runtime: "a compiled executable",
argv: ["bun", "/$bunfs/root/claude-agent-acp", "--hide-claude-auth"],
args: ["--hide-claude-auth"],
},
{
runtime: "a compiled Windows executable",
argv: ["bun", "B:\\~BUN\\root\\claude-agent-acp.exe", "--hide-claude-auth"],
args: ["--hide-claude-auth"],
},
])("reruns $runtime with its own flags", ({ argv, args }) => {
expect(agentCommand(argv)).toEqual({ command: process.execPath, args });
});
});

View File

@@ -168,7 +168,10 @@ describe("AsyncTaskRuntime", () => {
expect(published[1]?.update).not.toHaveProperty("summary");
expect(published.at(-1)?.update).toMatchObject({
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: "**Task stopped by user:** Build generated assets." },
content: {
type: "text",
text: "\n\n> **Task stopped by user:** Build generated assets.\n\n",
},
});
});
@@ -234,7 +237,7 @@ describe("AsyncTaskRuntime", () => {
]);
expect(published.at(-1)?.update).toMatchObject({
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: "**Task stopped by user:** npm run build." },
content: { type: "text", text: "\n\n> **Task stopped by user:** npm run build.\n\n" },
});
});
@@ -264,7 +267,7 @@ describe("AsyncTaskRuntime", () => {
});
expect(published.at(-1)?.update).toMatchObject({
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: "**Task stopped by user:** npm run build." },
content: { type: "text", text: "\n\n> **Task stopped by user:** npm run build.\n\n" },
});
});

View File

@@ -306,6 +306,14 @@ describe("auth status mappers", () => {
});
});
it("maps a setup-token CLI login without profile fields to an account", () => {
expect(
fromCliStatus(
JSON.stringify({ loggedIn: true, authMethod: "oauth_token", apiProvider: "firstParty" }),
),
).toEqual({ kind: "account", label: "Claude subscription", detail: "OAuth token" });
});
it("lets a third-party backend outrank the logged-out flag", () => {
// `loggedIn` tracks the claude.ai login only; on Bedrock the credentials
// are external and the agent is authenticated all the same.

View File

@@ -0,0 +1,129 @@
import { appendFile, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
BACKGROUND_OUTPUT_MAX_BACKLOG_BYTES,
BACKGROUND_OUTPUT_READ_BYTES,
type BackgroundOutput,
BackgroundOutputTail,
} from "../background-output.js";
describe("BackgroundOutputTail", () => {
let dir: string;
let file: string;
let published: BackgroundOutput[];
let tail: BackgroundOutputTail | undefined;
const follow = () =>
(tail = new BackgroundOutputTail(file, async (output) => void published.push(output)));
beforeEach(async () => {
dir = await mkdtemp(path.join(tmpdir(), "background-output-"));
file = path.join(dir, "bg1.output");
published = [];
});
afterEach(async () => {
tail?.stop();
await rm(dir, { recursive: true, force: true });
});
it("replaces the terminal's output with the file's first text, then appends what the file adds", async () => {
await writeFile(file, "building\n");
follow();
await vi.waitFor(() => expect(published).toHaveLength(1), { timeout: 2000 });
await appendFile(file, "done\n");
await vi.waitFor(() => expect(published).toHaveLength(2), { timeout: 2000 });
expect(published).toEqual([
{ text: "building\n", replaces: true },
{ text: "done\n", replaces: false },
]);
});
it("waits for a file that does not exist yet", async () => {
follow();
await new Promise((resolve) => setTimeout(resolve, 700));
expect(published).toEqual([]);
await writeFile(file, "started\n");
await vi.waitFor(() => expect(published).toHaveLength(1), { timeout: 2000 });
expect(published[0]).toEqual({ text: "started\n", replaces: true });
});
it("holds a character that a read splits until its last byte is written", async () => {
const check = Buffer.from("✓", "utf8");
await writeFile(file, check.subarray(0, 1));
follow();
await new Promise((resolve) => setTimeout(resolve, 700));
expect(published).toEqual([]);
await appendFile(file, check.subarray(1));
await tail!.finish();
expect(published).toEqual([{ text: "✓", replaces: true }]);
});
it("reads the rest of the file on finish, and nothing after", async () => {
await writeFile(file, "a");
follow();
await appendFile(file, "b");
await tail!.finish();
await appendFile(file, "c");
await new Promise((resolve) => setTimeout(resolve, 700));
expect(published.map((output) => output.text).join("")).toBe("ab");
});
it("sends at most the backlog, read by read, from the file's last bytes as a snapshot", async () => {
// More than the backlog, written faster than the polls read it.
const skipped = "x".repeat(BACKGROUND_OUTPUT_MAX_BACKLOG_BYTES);
const kept = "y".repeat(BACKGROUND_OUTPUT_MAX_BACKLOG_BYTES - 4) + "end\n";
await writeFile(file, skipped + kept);
follow();
await tail!.finish();
expect(published.map((output) => output.text).join("")).toBe(kept);
expect(published[0].replaces).toBe(true);
expect(published.slice(1).every((output) => !output.replaces)).toBe(true);
expect(
published.every((output) => Buffer.byteLength(output.text) <= BACKGROUND_OUTPUT_READ_BYTES),
).toBe(true);
});
it("starts over with a snapshot when the file shrinks", async () => {
await writeFile(file, "old output\n");
follow();
await vi.waitFor(() => expect(published).toHaveLength(1), { timeout: 2000 });
await writeFile(file, "new\n");
await tail!.finish();
expect(published).toEqual([
{ text: "old output\n", replaces: true },
{ text: "new\n", replaces: true },
]);
});
it("replaces the placeholder with an empty output when the command wrote nothing", async () => {
await writeFile(file, "");
follow();
await tail!.finish();
expect(published).toEqual([{ text: "", replaces: true }]);
});
it("publishes nothing on finish when the file never appeared", async () => {
follow();
await tail!.finish();
expect(published).toEqual([]);
});
it("reads nothing after stop", async () => {
follow();
tail!.stop();
await writeFile(file, "late\n");
await new Promise((resolve) => setTimeout(resolve, 700));
expect(published).toEqual([]);
});
});

View File

@@ -0,0 +1,57 @@
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// Fresh module state per test: the bundled CLI directory is process-wide.
async function load() {
vi.resetModules();
return import("../claude-cli.js");
}
describe("claudeCliPath", () => {
let dir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "claude-cli-"));
vi.stubEnv("CLAUDE_CODE_EXECUTABLE", "");
});
afterEach(() => {
vi.unstubAllEnvs();
rmSync(dir, { recursive: true, force: true });
});
const cliFileName = process.platform === "win32" ? "claude.exe" : "claude";
it("prefers CLAUDE_CODE_EXECUTABLE over the bundled CLI", async () => {
const { claudeCliPath, useBundledClaudeCli } = await load();
writeFileSync(join(dir, cliFileName), "");
useBundledClaudeCli(dir);
vi.stubEnv("CLAUDE_CODE_EXECUTABLE", "/opt/claude");
expect(await claudeCliPath()).toBe("/opt/claude");
});
it("returns the CLI next to the executable", async () => {
const { claudeCliPath, useBundledClaudeCli } = await load();
writeFileSync(join(dir, cliFileName), "");
useBundledClaudeCli(dir);
expect(await claudeCliPath()).toBe(join(dir, cliFileName));
});
it("throws when the CLI is missing next to the executable", async () => {
const { claudeCliPath, useBundledClaudeCli } = await load();
useBundledClaudeCli(dir);
await expect(claudeCliPath()).rejects.toThrow(
`The Claude CLI is missing: expected ${join(dir, cliFileName)}. Keep ${cliFileName} from the release archive`,
);
});
it.skipIf(!process.env.RUN_INTEGRATION_TESTS)("finds the installed CLI", async () => {
const { claudeCliPath } = await load();
expect(await claudeCliPath()).toMatch(/@anthropic-ai\/claude-agent-sdk-[^/]+\/claude(\.exe)?$/);
});
});

View File

@@ -4,6 +4,7 @@ import {
ContextCompactionLifecycle,
clientSupportsCompactionUpdates,
compactionSummaryText,
contextCompactionMetadataFromBoundary,
isCompactSummaryMessage,
} from "../context-compaction.js";
@@ -129,6 +130,17 @@ describe("compactionSummaryText", () => {
);
});
it("strips the framing when other content sits before the Summary line", () => {
expect(
compactionSummaryText(
"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" +
"<system-reminder>\n<tools-used-before-summary>Read, Bash</tools-used-before-summary>\n</system-reminder>\n" +
"Summary:\n1. Primary Request and Intent:\n Count upward.\n\n" +
"Continue the conversation from where it left off without asking the user any further questions.",
),
).toBe("1. Primary Request and Intent:\n Count upward.");
});
it("strips the older tagged transcript framing", () => {
expect(
compactionSummaryText(
@@ -635,3 +647,20 @@ describe("ContextCompactionLifecycle (tool_call)", () => {
]);
});
});
describe("contextCompactionMetadataFromBoundary", () => {
/** An adapter-sent compaction is `idle` only when the CLI calls it manual; the CLI's own auto trigger stays `automatic`. */
it.each([
{ cli: "manual", idleCompaction: false, expected: "manual" },
{ cli: "manual", idleCompaction: true, expected: "idle" },
{ cli: "auto", idleCompaction: true, expected: "automatic" },
] as const)(
"reports $expected for a $cli boundary (idleCompaction: $idleCompaction)",
({ cli, idleCompaction, expected }) => {
expect(
contextCompactionMetadataFromBoundary({ trigger: cli, pre_tokens: 1 }, { idleCompaction })
.trigger,
).toBe(expected);
},
);
});

View File

@@ -413,7 +413,7 @@ describe("syncFastModeState (SDK-driven state changes)", () => {
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: "**Fast mode turned off:** requires extra usage to be enabled for this account.",
text: "\n\n> **Fast mode turned off:** requires extra usage to be enabled for this account.\n\n",
},
});
expect(sessionUpdates[1].update).toMatchObject({ sessionUpdate: "config_option_update" });

View File

@@ -0,0 +1,555 @@
/**
* Idle compaction wired into the agent (see `idle-compaction.ts` for the
* timer's own rules): a scripted Claude Code answers each prompt it receives,
* and fake timers move the clock.
*/
import { randomUUID } from "node:crypto";
import { mkdtemp, rm } from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { Options, SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
import type { AcpClient, ClaudeAcpAgent } from "../acp-agent.js";
import { initializeClient } from "./helpers.js";
import { successfulResultMessage } from "./session-doubles.js";
const execFileSpy = vi.hoisted(() => vi.fn());
vi.mock("node:child_process", async () => {
const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process");
return { ...actual, execFile: execFileSpy };
});
/** What the mocked Claude Agent SDK answers. Reset before each test. */
const sdk = vi.hoisted(() => ({
run: async function* (
_prompts: AsyncIterable<SDKUserMessage>,
_options: Options,
): AsyncGenerator<Record<string, unknown>> {},
models: [
{ value: "claude-sonnet-4-6", displayName: "Claude Sonnet", description: "Fast" },
{ value: "claude-opus-4-6", displayName: "Claude Opus", description: "Smart" },
] as Array<Record<string, unknown>>,
}));
vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
const actual = await vi.importActual<typeof import("@anthropic-ai/claude-agent-sdk")>(
"@anthropic-ai/claude-agent-sdk",
);
const { makeMockQuery } = await import("./helpers.js");
return {
...actual,
query: ({ prompt, options }: { prompt: AsyncIterable<SDKUserMessage>; options: Options }) => {
const messages = sdk.run(prompt, options);
const controls = makeMockQuery({
[Symbol.asyncIterator]: () => messages,
initializationResult: async () => ({ models: sdk.models }),
});
return Object.assign(messages, controls);
},
getSessionMessages: vi.fn(async () => []),
};
});
vi.mock("../tools.js", async () => ({
...(await vi.importActual<typeof import("../tools.js")>("../tools.js")),
registerHookCallback: vi.fn(),
}));
const MINUTE_MS = 60_000;
const AIR_CLIENT_CAPABILITIES = {
_meta: { jetbrains: { air: { version: 1, capabilities: [] as string[] } } },
};
/** As the CLI reports a request whose prompt cache lives one hour: 250k tokens of context. */
const LARGE_ONE_HOUR_USAGE = {
input_tokens: 10,
output_tokens: 5,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 250_000,
cache_creation: { ephemeral_1h_input_tokens: 250_000, ephemeral_5m_input_tokens: 0 },
};
let tempDir: string;
let cwd: string;
beforeEach(async () => {
execFileSpy.mockImplementation((...invocation: unknown[]) => {
const callback = invocation[invocation.length - 1] as (...a: unknown[]) => void;
callback(null, { stdout: "", stderr: "" });
});
tempDir = await mkdtemp(path.join(os.tmpdir(), "idle-compaction-"));
cwd = await mkdtemp(path.join(tempDir, "project-"));
vi.stubEnv("CLAUDE_CONFIG_DIR", path.join(tempDir, "claude"));
vi.stubEnv("CLAUDE_AGENT_ACP_IDLE_COMPACT_DELAY_MS", "");
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "Date"] });
});
afterEach(async () => {
vi.useRealTimers();
execFileSpy.mockReset();
vi.unstubAllEnvs();
await rm(tempDir, { recursive: true, force: true, maxRetries: 10 });
});
function assistant(
options: Options,
usage: Record<string, unknown>,
parent: string | null = null,
id = `msg_${randomUUID()}`,
) {
return {
type: "assistant",
message: {
id,
type: "message",
role: "assistant",
model: "claude-sonnet-4-6",
content: [{ type: "text", text: "Done." }],
stop_reason: "end_turn",
stop_sequence: null,
usage,
},
parent_tool_use_id: parent,
uuid: randomUUID(),
session_id: options.sessionId,
};
}
function system(options: Options, subtype: string, fields: Record<string, unknown>) {
return { type: "system", subtype, ...fields, uuid: randomUUID(), session_id: options.sessionId };
}
/** The `user` message Claude Code replays for a prompt it took in. */
function echo(value: SDKUserMessage, options: Options) {
return {
type: "user",
message: value.message,
parent_tool_use_id: null,
uuid: value.uuid,
session_id: options.sessionId,
isReplay: true,
};
}
/**
* Claude Code takes in each prompt in order. A prompt answers with one
* main-thread request (with `usage`, after `during`) and goes idle; `/compact`
* answers with a compaction, as the CLI reports a manual one, and goes idle.
* `received` lists the texts that Claude Code took in.
*/
function scriptClaudeCode({
usage = LARGE_ONE_HOUR_USAGE,
during,
compactResult = "success",
suggestion,
responseTakesMs,
}: {
usage?: Record<string, unknown>;
during?: (options: Options) => AsyncGenerator<any>;
compactResult?: "success" | "failed";
/** The prompt the CLI predicts after each answered prompt, as it does after a result. */
suggestion?: string;
/** Streams the answer: `message_start` first, then the `assistant` message (same id) this long after. */
responseTakesMs?: number;
} = {}) {
const received: string[] = [];
/** Resolves each time the consumer has handled an `idle` message. */
let idleHandled = Promise.withResolvers<void>();
sdk.run = async function* (prompts, options) {
const input = prompts[Symbol.asyncIterator]();
for (;;) {
const { value, done } = await input.next();
if (done) return;
const text = (value.message.content as Array<{ text: string }>)[0].text;
received.push(text);
if (text === "/compact") {
yield system(options, "status", { status: "compacting" });
if (compactResult === "success") {
yield system(options, "compact_boundary", {
compact_metadata: { trigger: "manual", pre_tokens: 250_000, post_tokens: 12_000 },
});
}
yield system(options, "status", {
status: null,
compact_result: compactResult,
...(compactResult === "failed" ? { compact_error: "too short" } : {}),
});
} else if (text === "/clear") {
yield echo(value, options);
yield {
type: "conversation_reset",
new_conversation_id: randomUUID(),
uuid: randomUUID(),
session_id: options.sessionId,
};
} else {
yield echo(value, options);
if (during) yield* during(options);
const id = `msg_${randomUUID()}`;
if (responseTakesMs !== undefined) {
yield {
type: "stream_event",
event: {
type: "message_start",
message: { id, role: "assistant", model: "claude-sonnet-4-6", content: [], usage },
},
parent_tool_use_id: null,
uuid: randomUUID(),
session_id: options.sessionId,
};
await vi.advanceTimersByTimeAsync(responseTakesMs);
}
yield assistant(options, usage, null, id);
}
yield successfulResultMessage({ session_id: options.sessionId });
if (suggestion && text !== "/compact") {
yield {
type: "prompt_suggestion",
suggestion,
uuid: randomUUID(),
session_id: options.sessionId,
};
}
yield system(options, "session_state_changed", { state: "idle" });
idleHandled.resolve();
}
};
return {
received,
/** Waits until the consumer has handled an `idle`, then re-arms for the next. */
async untilIdle() {
await idleHandled.promise;
idleHandled = Promise.withResolvers();
},
};
}
async function connect(clientCapabilities: Record<string, unknown> = {}) {
vi.resetModules();
const { ClaudeAcpAgent } = await import("../acp-agent.js");
const updates: Array<{ update: Record<string, any> }> = [];
const client = {
sessionUpdate: async (notification: { update: Record<string, any> }) => {
updates.push(notification);
},
} as unknown as AcpClient;
const logs: string[] = [];
const agent: ClaudeAcpAgent = new ClaudeAcpAgent(client, {
log: (message: string) => logs.push(message),
error: () => {},
});
await initializeClient(agent, clientCapabilities);
const session = await agent.newSession({ cwd, mcpServers: [] });
return { agent, sessionId: session.sessionId, updates, logs };
}
async function promptHi(agent: ClaudeAcpAgent, id: string) {
await agent.prompt({ sessionId: id, prompt: [{ type: "text", text: "hi" }] });
}
describe("idle compaction in the agent", () => {
/** Idle after a large 1h request: `/compact` goes into the input at 54 minutes, not before. */
it("compacts an idle session with a large context shortly before its one-hour cache expires, with no client prompt", async () => {
const claude = scriptClaudeCode();
const { agent, sessionId } = await connect();
await promptHi(agent, sessionId);
await claude.untilIdle();
await vi.advanceTimersByTimeAsync(54 * MINUTE_MS - 1);
expect(claude.received).toEqual(["hi"]);
await vi.advanceTimersByTimeAsync(1);
expect(claude.received).toEqual(["hi", "/compact"]);
await agent.dispose();
});
/** A client prompt 30 minutes in moves the deadline, so 54 minutes after the first request passes quietly. */
it("does not compact when a client prompt comes during the wait", async () => {
const claude = scriptClaudeCode();
const { agent, sessionId } = await connect();
await promptHi(agent, sessionId);
await claude.untilIdle();
await vi.advanceTimersByTimeAsync(30 * MINUTE_MS);
await promptHi(agent, sessionId);
await claude.untilIdle();
// The second request moved the deadline: 54 minutes after the first has passed.
await vi.advanceTimersByTimeAsync(30 * MINUTE_MS);
expect(claude.received).toEqual(["hi", "hi"]);
await agent.dispose();
});
/** Only a subagent's request reports 1h, so the main thread has no TTL and nothing arms. */
it("does not take a subagent's request for the session's cache TTL", async () => {
const claude = scriptClaudeCode({
// The main thread's large request wrote no cache; only the subagent's request reports 1h.
usage: {
input_tokens: 10,
output_tokens: 5,
cache_read_input_tokens: 250_000,
cache_creation_input_tokens: 0,
cache_creation: { ephemeral_1h_input_tokens: 0, ephemeral_5m_input_tokens: 0 },
},
during: async function* (options) {
yield assistant(options, LARGE_ONE_HOUR_USAGE, "toolu_agent");
},
});
const { agent, sessionId } = await connect();
await promptHi(agent, sessionId);
await claude.untilIdle();
await vi.advanceTimersByTimeAsync(2 * 60 * MINUTE_MS);
expect(claude.received).toEqual(["hi"]);
await agent.dispose();
});
/** Switching the model during the wait drops the timer. */
it("does not compact when the model changes during the wait", async () => {
const claude = scriptClaudeCode();
const { agent, sessionId } = await connect();
await promptHi(agent, sessionId);
await claude.untilIdle();
await vi.advanceTimersByTimeAsync(10 * MINUTE_MS);
await agent.setSessionConfigOption({ sessionId, configId: "model", value: "claude-opus-4-6" });
await vi.advanceTimersByTimeAsync(2 * 60 * MINUTE_MS);
expect(claude.received).toEqual(["hi"]);
await agent.dispose();
});
/** Closing the session during the wait drops the timer. */
it("does not compact once the session is closed", async () => {
const claude = scriptClaudeCode();
const { agent, sessionId } = await connect();
await promptHi(agent, sessionId);
await claude.untilIdle();
await agent.closeSession({ sessionId });
await vi.advanceTimersByTimeAsync(2 * 60 * MINUTE_MS);
expect(claude.received).toEqual(["hi"]);
});
/** A failed `/compact` is not repeated while no new request arrives. */
it("does not compact again for the same request after a compaction failed", async () => {
const claude = scriptClaudeCode({ compactResult: "failed" });
const { agent, sessionId, logs } = await connect();
await promptHi(agent, sessionId);
await claude.untilIdle();
await vi.advanceTimersByTimeAsync(54 * MINUTE_MS);
await claude.untilIdle();
await vi.advanceTimersByTimeAsync(3 * 60 * MINUTE_MS);
expect(claude.received).toEqual(["hi", "/compact"]);
expect(logs).toContain("[idle-compaction] compaction failed: too short");
await agent.dispose();
});
/** A `/clear` during the wait forgets the request, so the next idle arms nothing. */
it("does not compact for a request that a /clear dropped", async () => {
const claude = scriptClaudeCode();
const { agent, sessionId } = await connect();
await promptHi(agent, sessionId);
await claude.untilIdle();
await vi.advanceTimersByTimeAsync(10 * MINUTE_MS);
await agent.prompt({ sessionId, prompt: [{ type: "text", text: "/clear" }] });
await claude.untilIdle();
await vi.advanceTimersByTimeAsync(3 * 60 * MINUTE_MS);
expect(claude.received).toEqual(["hi", "/clear"]);
await agent.dispose();
});
/** The steered turn settles at the idle, so only an `onIdle` after that settle can arm. */
it("arms after the idle that settles a steered turn", async () => {
const received: string[] = [];
const release = Promise.withResolvers<void>();
const idleHandled = Promise.withResolvers<void>();
sdk.run = async function* (prompts, options) {
const input = prompts[Symbol.asyncIterator]();
const { value: first } = await input.next();
received.push((first.message.content as Array<{ text: string }>)[0].text);
yield echo(first, options);
await release.promise;
yield assistant(options, LARGE_ONE_HOUR_USAGE);
yield successfulResultMessage({ session_id: options.sessionId });
const { value: steer } = await input.next();
received.push((steer.message.content as Array<{ text: string }>)[0].text);
yield echo(steer, options);
yield assistant(options, LARGE_ONE_HOUR_USAGE);
yield successfulResultMessage({ session_id: options.sessionId });
yield system(options, "session_state_changed", { state: "idle" });
idleHandled.resolve();
for (;;) {
const { value, done } = await input.next();
if (done) return;
received.push((value.message.content as Array<{ text: string }>)[0].text);
}
};
const { agent, sessionId } = await connect();
const prompted = promptHi(agent, sessionId);
await vi.advanceTimersByTimeAsync(0);
await agent.steer({ sessionId, prompt: [{ type: "text", text: "also X" }] });
release.resolve();
await prompted;
await idleHandled.promise;
await vi.advanceTimersByTimeAsync(54 * MINUTE_MS);
expect(received).toEqual(["hi", "also X", "/compact"]);
await agent.dispose();
});
/** The idle `/compact` is not the prompt the user is looking at a prediction for, so the prediction stays. */
it("keeps the published prompt suggestion through the idle compaction", async () => {
const claude = scriptClaudeCode({ suggestion: "Run the tests" });
const { agent, sessionId, updates } = await connect(AIR_CLIENT_CAPABILITIES);
await promptHi(agent, sessionId);
await claude.untilIdle();
await vi.advanceTimersByTimeAsync(54 * MINUTE_MS);
await claude.untilIdle();
expect(claude.received).toEqual(["hi", "/compact"]);
expect(promptSuggestions(updates)).toEqual([{ suggestions: [{ text: "Run the tests" }] }]);
await agent.dispose();
});
/** A prompt still waiting on a provider update when the timer fires disarmed it first, so no `/compact` lands before the prompt. */
it("does not compact behind a prompt that is waiting on a provider update", async () => {
const claude = scriptClaudeCode();
const { agent, sessionId, logs } = await connect();
await promptHi(agent, sessionId);
await claude.untilIdle();
const update = Promise.withResolvers<void>();
(agent as any).providerUpdate = update.promise;
const waiting = promptHi(agent, sessionId);
await vi.advanceTimersByTimeAsync(54 * MINUTE_MS);
expect(claude.received).toEqual(["hi"]);
expect(logs).not.toContain("[idle-compaction] firing /compact");
expect(logs).toContain("[idle-compaction] disarmed: prompt");
(agent as any).providerUpdate = null;
update.resolve();
await waiting;
expect(claude.received).toEqual(["hi", "hi"]);
await agent.dispose();
});
/** The idle `/compact` is not a user prompt, so it asks for no title; a client prompt in the same session does. */
it("does not feed the idle compaction's prompt to the title machinery", async () => {
const claude = scriptClaudeCode();
const { agent, sessionId } = await connect();
const titles = agent.sessions[sessionId].titles;
const onPrompt = vi.spyOn(titles, "onPrompt");
const onPromptQueued = vi.spyOn(titles, "onPromptQueued");
await promptHi(agent, sessionId);
await claude.untilIdle();
expect(onPrompt).toHaveBeenCalledTimes(1);
expect(onPromptQueued).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(54 * MINUTE_MS);
await claude.untilIdle();
expect(claude.received).toEqual(["hi", "/compact"]);
expect(onPrompt).toHaveBeenCalledTimes(1);
expect(onPromptQueued).toHaveBeenCalledTimes(1);
await agent.dispose();
});
/** A rate limit warning that arrives mid-turn, before the turn's first request, still skips the compaction at the deadline. */
it("does not compact after a rate limit warning", async () => {
const claude = scriptClaudeCode({
during: async function* (options) {
yield {
type: "rate_limit_event",
rate_limit_info: { status: "allowed_warning", resetsAt: 1700000000, utilization: 0.9 },
uuid: randomUUID(),
session_id: options.sessionId,
};
},
});
const { agent, sessionId, logs } = await connect();
await promptHi(agent, sessionId);
await claude.untilIdle();
await vi.advanceTimersByTimeAsync(2 * 60 * MINUTE_MS);
expect(claude.received).toEqual(["hi"]);
expect(logs).toContain("[idle-compaction] skip: rate limit status is allowed_warning");
await agent.dispose();
});
/** The cache is written when the request starts, so a 7-minute answer still compacts 54 minutes after `message_start`, not after its last block. */
it("measures the deadline from the start of a long streamed response", async () => {
const claude = scriptClaudeCode({ responseTakesMs: 7 * MINUTE_MS });
const { agent, sessionId } = await connect();
await promptHi(agent, sessionId);
await claude.untilIdle();
await vi.advanceTimersByTimeAsync(47 * MINUTE_MS - 1);
expect(claude.received).toEqual(["hi"]);
await vi.advanceTimersByTimeAsync(1);
expect(claude.received).toEqual(["hi", "/compact"]);
await agent.dispose();
});
/** A timer that fires hours late (the machine slept) finds a cold cache: compacting would write the whole context. */
it("does not compact when the clock jumped past the deadline before the timer fired", async () => {
const claude = scriptClaudeCode();
const { agent, sessionId } = await connect();
await promptHi(agent, sessionId);
await claude.untilIdle();
vi.setSystemTime(Date.now() + 8 * 60 * MINUTE_MS);
await vi.advanceTimersByTimeAsync(54 * MINUTE_MS);
expect(claude.received).toEqual(["hi"]);
await agent.dispose();
});
/** AIR gets `trigger: "idle"` for the adapter's compaction and `"manual"` for a client's `/compact`. */
it.each([
{ name: "an idle compaction", prompt: "hi", expected: "idle" },
{ name: "a manual /compact", prompt: "/compact", expected: "manual" },
])("tells AIR that $name has trigger $expected", async ({ prompt, expected }) => {
const claude = scriptClaudeCode();
const { agent, sessionId, updates } = await connect(AIR_CLIENT_CAPABILITIES);
if (expected === "idle") {
await promptHi(agent, sessionId);
await claude.untilIdle();
await vi.advanceTimersByTimeAsync(54 * MINUTE_MS);
await vi.waitFor(() => expect(compactionTriggers(updates)).toHaveLength(1));
} else {
await agent.prompt({ sessionId, prompt: [{ type: "text", text: prompt }] });
}
expect(compactionTriggers(updates)).toEqual([expected]);
await agent.dispose();
});
});
/** Each `promptSuggestions` value that the client received. */
function promptSuggestions(updates: Array<{ update: Record<string, any> }>) {
return updates.flatMap(({ update }) => {
const suggestions = update._meta?.promptSuggestions;
return suggestions ? [suggestions] : [];
});
}
/** The `trigger` of each compaction report that the client received. */
function compactionTriggers(updates: Array<{ update: Record<string, any> }>) {
return updates.flatMap(({ update }) => {
const trigger = update._meta?.jetbrains?.air?.contextCompaction?.trigger;
return trigger ? [trigger] : [];
});
}

View File

@@ -0,0 +1,416 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { IdleCompaction, type IdleCompactionHost } from "../idle-compaction.js";
const MIN = 60_000;
const ONE_HOUR = {
cache_creation: { ephemeral_1h_input_tokens: 1000, ephemeral_5m_input_tokens: 0 },
};
const FIVE_MIN = {
cache_creation: { ephemeral_1h_input_tokens: 0, ephemeral_5m_input_tokens: 1000 },
};
const NOTHING_WRITTEN = {
cache_creation: { ephemeral_1h_input_tokens: 0, ephemeral_5m_input_tokens: 0 },
};
function setup(
options: {
env?: Record<string, string | undefined>;
tokens?: number;
settings?: ReturnType<IdleCompactionHost["settings"]>;
setTimeout?: (callback: () => void, ms: number) => unknown;
} = {},
) {
const state = {
busy: false,
tokens: options.tokens ?? 200_000,
settings: options.settings ?? {},
};
const host = {
isBusy: vi.fn(() => state.busy),
contextTokens: vi.fn(() => state.tokens),
settings: vi.fn(() => state.settings),
compact: vi.fn(async (): Promise<unknown> => undefined),
log: vi.fn(),
};
const idle = new IdleCompaction(host, { env: options.env ?? {}, setTimeout: options.setTimeout });
return { idle, host, state };
}
/** Records a 1h-cache request and goes idle. */
function requestAndIdle(idle: IdleCompaction, usage: unknown = ONE_HOUR) {
idle.noteRequest(usage);
idle.onIdle();
}
describe("IdleCompaction", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
/** A failed compaction is logged and must not re-arm from the same request, or it loops until the user prompts. */
it("does not arm again for a request it already compacted for", async () => {
const { idle, host } = setup();
host.compact.mockRejectedValue(new Error("boom"));
requestAndIdle(idle);
await vi.advanceTimersByTimeAsync(54 * MIN);
expect(host.compact).toHaveBeenCalledTimes(1);
expect(host.log).toHaveBeenCalledWith(expect.stringContaining("boom"));
idle.onIdle();
await vi.advanceTimersByTimeAsync(2 * 60 * MIN);
expect(host.compact).toHaveBeenCalledTimes(1);
requestAndIdle(idle);
await vi.advanceTimersByTimeAsync(54 * MIN);
expect(host.compact).toHaveBeenCalledTimes(2);
});
/** `/clear` replaces the conversation: what was recorded no longer describes it. */
it("does not arm after reset until a new request", async () => {
const { idle, host } = setup();
requestAndIdle(idle);
idle.reset();
idle.onIdle();
await vi.advanceTimersByTimeAsync(2 * 60 * MIN);
expect(host.compact).not.toHaveBeenCalled();
requestAndIdle(idle);
await vi.advanceTimersByTimeAsync(54 * MIN);
expect(host.compact).toHaveBeenCalledTimes(1);
});
/** A reported failure is logged only while the adapter's own compaction runs. */
it("logs a reported compaction failure only during its own compaction", async () => {
const { idle, host } = setup();
const running = Promise.withResolvers<void>();
host.compact.mockReturnValue(running.promise);
requestAndIdle(idle);
idle.noteCompactionFailed("too short");
expect(host.log).not.toHaveBeenCalledWith(expect.stringContaining("compaction failed"));
await vi.advanceTimersByTimeAsync(54 * MIN);
idle.noteCompactionFailed("too short");
expect(host.log).toHaveBeenCalledWith("[idle-compaction] compaction failed: too short");
});
/** `isCompacting` is true exactly while the compaction it started runs. */
it("reports isCompacting while its compaction runs", async () => {
const { idle, host } = setup();
const running = Promise.withResolvers<void>();
host.compact.mockReturnValue(running.promise);
requestAndIdle(idle);
expect(idle.isCompacting).toBe(false);
await vi.advanceTimersByTimeAsync(54 * MIN);
expect(idle.isCompacting).toBe(true);
running.resolve();
await vi.advanceTimersByTimeAsync(0);
expect(idle.isCompacting).toBe(false);
});
/** A 1h-cache request with 200k context compacts at 54 minutes, not before. */
it("arms and compacts at 54 minutes", async () => {
const { idle, host } = setup();
requestAndIdle(idle);
await vi.advanceTimersByTimeAsync(54 * MIN - 1);
expect(host.compact).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(1);
expect(host.compact).toHaveBeenCalledTimes(1);
});
/** The deadline counts from the request, not from when the session went idle. */
it("measures the deadline from the request time", async () => {
const { idle, host } = setup();
idle.noteRequest(ONE_HOUR);
await vi.advanceTimersByTimeAsync(10 * MIN);
idle.onIdle();
await vi.advanceTimersByTimeAsync(44 * MIN);
expect(host.compact).toHaveBeenCalledTimes(1);
});
/** A cache whose deadline passed more than 60 s ago is cold: a timer that fires that late (the machine slept) compacts nothing. */
it.each([
["the timer fires 61 s late", 61_000, false],
["the timer fires 60 s late", 60_000, true],
])("when %s, compacts: %s", async (_name, lateMs, compacts) => {
const { idle, host } = setup();
requestAndIdle(idle);
vi.setSystemTime(Date.now() + lateMs);
await vi.advanceTimersByTimeAsync(54 * MIN);
expect(host.compact).toHaveBeenCalledTimes(compacts ? 1 : 0);
});
/** Going idle after the deadline is more than 60 s past arms nothing. */
it("does not arm when the deadline passed more than 60 s ago", async () => {
const { idle, host } = setup();
idle.noteRequest(ONE_HOUR);
await vi.advanceTimersByTimeAsync(54 * MIN + 61_000);
idle.onIdle();
expect(vi.getTimerCount()).toBe(0);
await vi.advanceTimersByTimeAsync(0);
expect(host.compact).not.toHaveBeenCalled();
expect(host.log).toHaveBeenCalledWith(expect.stringContaining("lapsed"));
});
/** The rate limit status is the last one reported: a reset or a malformed event keeps `rejected`, a later `allowed` clears it. */
it("keeps the rate limit status across a reset and a malformed event until an allowed one", async () => {
const { idle, host } = setup();
idle.noteRateLimit({ status: "rejected" });
idle.noteRateLimit(null);
idle.reset();
requestAndIdle(idle);
expect(vi.getTimerCount()).toBe(0);
idle.noteRateLimit({ status: "allowed" });
idle.onIdle();
await vi.advanceTimersByTimeAsync(54 * MIN);
expect(host.compact).toHaveBeenCalledTimes(1);
});
/** Later blocks of one response (same message id) keep the deadline from its first sighting; a new id moves it. */
it.each([
["the same message id", "msg_1", 54 * MIN],
["a new message id", "msg_2", 54 * MIN + 7 * MIN],
])("a request with %s: the deadline stays or moves", async (_name, id, firesAt) => {
const { idle, host } = setup();
idle.noteRequest(ONE_HOUR, "msg_1");
await vi.advanceTimersByTimeAsync(7 * MIN);
idle.noteRequest(ONE_HOUR, id);
idle.onIdle();
await vi.advanceTimersByTimeAsync(firesAt - 7 * MIN - 1);
expect(host.compact).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(1);
expect(host.compact).toHaveBeenCalledTimes(1);
});
/** A deadline already in the past fires immediately. */
it("fires right away when the deadline has passed", async () => {
const { idle, host } = setup();
idle.noteRequest(ONE_HOUR);
await vi.advanceTimersByTimeAsync(55 * MIN);
idle.onIdle();
await vi.advanceTimersByTimeAsync(0);
expect(host.compact).toHaveBeenCalledTimes(1);
});
/** Each condition that fails R2 leaves no timer armed. */
it.each([
["5m TTL", { usage: FIVE_MIN }],
["unknown TTL", { usage: {} }],
["malformed usage", { usage: null }],
["context below the minimum", { tokens: 199_999 }],
["idleCompaction: false", { settings: { idleCompaction: false } }],
["autoCompactEnabled: false", { settings: { autoCompactEnabled: false } }],
["DISABLE_COMPACT", { env: { DISABLE_COMPACT: "1" } }],
["DISABLE_AUTO_COMPACT", { env: { DISABLE_AUTO_COMPACT: "true" } }],
// Claude Code also reads these from the `env` block of settings.json.
[
"DISABLE_AUTO_COMPACT in the settings env",
{ settings: { env: { DISABLE_AUTO_COMPACT: "1" } } },
],
["rate limit status allowed_warning", { rateLimit: "allowed_warning" }],
["rate limit status rejected", { rateLimit: "rejected" }],
[
"CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS in the settings env",
{ tokens: 250_000, settings: { env: { CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS: "500000" } } },
],
[
"CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS in the settings env over the launch env",
{
tokens: 250_000,
env: { CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS: "100000" },
settings: { env: { CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS: "500000" } },
},
],
])("does not arm: %s", async (_name, options0: Record<string, any>) => {
const { usage, rateLimit, ...options } = options0;
const { idle, host } = setup(options);
if (rateLimit) idle.noteRateLimit({ status: rateLimit });
requestAndIdle(idle, "usage" in options0 ? usage : ONE_HOUR);
await vi.advanceTimersByTimeAsync(2 * 60 * MIN);
expect(host.compact).not.toHaveBeenCalled();
expect(vi.getTimerCount()).toBe(0);
expect(host.log).toHaveBeenCalledWith(expect.stringContaining("skip"));
});
/** A falsy value is not an opt-out, and the settings env is applied after the launch env, so its value wins, not an OR of the two. */
it.each([
["DISABLE_COMPACT=0 in the launch env", { env: { DISABLE_COMPACT: "0" } }],
[
"DISABLE_AUTO_COMPACT=0 in the settings env over =1 in the launch env",
{
env: { DISABLE_AUTO_COMPACT: "1" },
settings: { env: { DISABLE_AUTO_COMPACT: "0" } },
},
],
])("arms: %s", async (_name, options) => {
const { idle, host } = setup(options);
requestAndIdle(idle);
await vi.advanceTimersByTimeAsync(54 * MIN);
expect(host.compact).toHaveBeenCalledTimes(1);
});
/** Idle without any request in this instance (resumed session) arms nothing. */
it("does not arm before the first request", () => {
const { idle, host } = setup();
idle.onIdle();
expect(vi.getTimerCount()).toBe(0);
expect(host.log).toHaveBeenCalledWith(expect.stringContaining("no request recorded"));
});
/** A request that writes no cache keeps the previous request's TTL. */
it("keeps the previous TTL when the request writes nothing", async () => {
const { idle, host } = setup();
idle.noteRequest(ONE_HOUR);
requestAndIdle(idle, NOTHING_WRITTEN);
await vi.advanceTimersByTimeAsync(54 * MIN);
expect(host.compact).toHaveBeenCalledTimes(1);
});
/** A 5m request after a 1h one switches the TTL, so nothing arms. */
it("switches to 5m when a later request writes a 5m cache", () => {
const { idle } = setup();
idle.noteRequest(ONE_HOUR);
requestAndIdle(idle, FIVE_MIN);
expect(vi.getTimerCount()).toBe(0);
});
/** The min-tokens env raises the threshold and is clamped to 100k from below. */
it.each([
["300000", 299_999, false],
["300000", 300_000, true],
["50000", 99_999, false],
["50000", 100_000, true],
["abc", 200_000, true],
["abc", 199_999, false],
])("CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS=%s with %i tokens arms: %s", (min, tokens, arms) => {
const { idle } = setup({ env: { CLAUDE_CODE_IDLE_COMPACT_MIN_TOKENS: min }, tokens });
requestAndIdle(idle);
expect(vi.getTimerCount()).toBe(arms ? 1 : 0);
});
/** The delay env overrides the 54 minute default; invalid values fall back and are logged once. */
it.each([
["1000", 1000, false],
["0", 0, false],
["-5", 54 * MIN, true],
["soon", 54 * MIN, true],
["2147483648", 54 * MIN, true],
])("CLAUDE_AGENT_ACP_IDLE_COMPACT_DELAY_MS=%s fires after %ims", async (value, ms, invalid) => {
const { idle, host } = setup({ env: { CLAUDE_AGENT_ACP_IDLE_COMPACT_DELAY_MS: value } });
requestAndIdle(idle);
await vi.advanceTimersByTimeAsync(ms - 1 < 0 ? 0 : ms - 1);
if (ms > 0) expect(host.compact).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(1);
expect(host.compact).toHaveBeenCalledTimes(1);
const warnings = host.log.mock.calls.filter(([m]) => String(m).includes("invalid"));
expect(warnings).toHaveLength(invalid ? 1 : 0);
});
/** A new request drops the armed timer, so the deadline cannot fire for the old one; disarm and dispose drop it too. */
it.each([
["noteRequest", (idle: IdleCompaction) => idle.noteRequest(ONE_HOUR)],
["disarm", (idle: IdleCompaction) => idle.disarm("prompt")],
["dispose", (idle: IdleCompaction) => idle.dispose()],
])("%s drops the timer", async (_name, drop) => {
const { idle, host } = setup();
requestAndIdle(idle);
drop(idle);
expect(vi.getTimerCount()).toBe(0);
await vi.advanceTimersByTimeAsync(60 * MIN);
expect(host.compact).not.toHaveBeenCalled();
});
/** After dispose, requests and idle notifications do nothing. */
it("ignores calls after dispose", () => {
const { idle } = setup();
idle.dispose();
requestAndIdle(idle);
expect(vi.getTimerCount()).toBe(0);
});
/** A change after arming (busy, context shrunk, rate limit reached) is re-checked at the deadline: nothing is compacted and nothing re-arms. */
it.each([
["the session became busy", "busy", (ctx: ReturnType<typeof setup>) => (ctx.state.busy = true)],
[
"the context shrank below the minimum",
"below the",
(ctx: ReturnType<typeof setup>) => (ctx.state.tokens = 50_000),
],
[
"the rate limit status became rejected",
"rate limit",
(ctx: ReturnType<typeof setup>) => ctx.idle.noteRateLimit({ status: "rejected" }),
],
])("does not compact at the deadline when %s", async (_name, logged, change) => {
const ctx = setup();
requestAndIdle(ctx.idle);
change(ctx);
await vi.advanceTimersByTimeAsync(54 * MIN);
expect(ctx.host.compact).not.toHaveBeenCalled();
expect(ctx.host.log).toHaveBeenCalledWith(expect.stringContaining(logged));
});
/** Going idle while busy arms nothing. */
it("does not arm while busy", () => {
const { idle, state } = setup();
state.busy = true;
requestAndIdle(idle);
expect(vi.getTimerCount()).toBe(0);
});
/** Arming twice leaves a single timer, so only one compaction runs. */
it("rearming replaces the timer", async () => {
const { idle, host } = setup();
requestAndIdle(idle);
idle.onIdle();
expect(vi.getTimerCount()).toBe(1);
await vi.advanceTimersByTimeAsync(54 * MIN);
expect(host.compact).toHaveBeenCalledTimes(1);
});
/** While a compaction runs, an idle notification does not arm another. */
it("does not arm while a compaction is in flight", async () => {
const { idle, host } = setup();
let finish!: () => void;
host.compact.mockReturnValue(new Promise<void>((resolve) => (finish = resolve)));
requestAndIdle(idle);
await vi.advanceTimersByTimeAsync(54 * MIN);
idle.onIdle();
expect(vi.getTimerCount()).toBe(0);
finish();
});
/** The timer handle is unref'd so it never keeps the process alive. */
it("unrefs the timer", () => {
const unref = vi.fn();
const setTimeoutSpy = vi.fn(() => ({ unref }));
const { idle } = setup({ setTimeout: setTimeoutSpy });
requestAndIdle(idle);
expect(setTimeoutSpy).toHaveBeenCalledTimes(1);
expect(unref).toHaveBeenCalledTimes(1);
});
});

View File

@@ -381,6 +381,15 @@ describe("formatMcpStatus", () => {
expect(markdown).toContain("- ``a`b``\n");
expect(markdown).toContain("**Disabled:** `` `edge ``");
});
/** A name with a space at each end is padded so CommonMark keeps both spaces;
* an all-space name is not, since CommonMark does not strip it. */
it.each([
[" db ", "- ` db `"],
[" ", "- ` `"],
])("pads the server name %j only when CommonMark would strip it", (name, line) => {
expect(formatMcpStatus([{ name, status: "connected" }])).toContain(line);
});
});
describe("cleanMcpError", () => {

View File

@@ -7,6 +7,7 @@
import { vi } from "vitest";
import { randomUUID } from "crypto";
import { SessionBackgroundWork } from "../background-work.js";
import { SessionTitles } from "../session-titles.js";
/** Stand-in agent for a `SessionTitles` whose test doesn't care about titles:
@@ -91,6 +92,7 @@ export function mockSessionState(
toolUseCache: {},
emittedToolCalls: new Set(),
liveBackgroundTasks: new Map(),
background: new SessionBackgroundWork(sessionId, false),
emittedAssistantText: false,
owedTrailingIdles: 0,
messageIdToUuid: new Map(),

View File

@@ -112,7 +112,7 @@ describe("session mode", () => {
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: "**Auto mode unavailable:** the selected model does not support Auto mode; using Accept edits instead.",
text: "\n\n> **Auto mode unavailable:** the selected model does not support Auto mode; using Accept edits instead.\n\n",
},
});
});
@@ -174,6 +174,7 @@ describe("session mode", () => {
requestedMode: "default",
currentModelId: "opus",
allowBypass: false,
allowAuto: true,
});
expect(result.modes.availableModes.map((mode) => mode.id)).not.toContain("bypassPermissions");
@@ -188,6 +189,7 @@ describe("session mode", () => {
currentModelInfo: { value: "haiku", displayName: "Haiku", description: "" },
currentModelId: "haiku",
allowBypass: true,
allowAuto: true,
});
expect(result.modes.currentModeId).toBe("acceptEdits");
@@ -199,6 +201,24 @@ describe("session mode", () => {
expect(notifications.logError).toHaveBeenCalledWith(expect.stringContaining("falling back"));
});
it("starts a built-in Auto default in Manual on a model without Auto", async () => {
const setPermissionMode = vi.fn();
const { manager } = createManager(undefined);
const result = await manager.initialize({
query: { setPermissionMode },
requestedMode: "auto",
builtInDefault: true,
currentModelInfo: { value: "haiku", displayName: "Haiku", description: "" },
currentModelId: "haiku",
allowBypass: true,
allowAuto: true,
});
expect(result.modes.currentModeId).toBe("default");
expect(result.autoModeFallbackWarningPending).toBe(false);
expect(setPermissionMode).toHaveBeenCalledWith("default");
});
it("keeps mode state and its config option synchronized", () => {
const session = {
...createSession(),
@@ -248,24 +268,46 @@ describe("session mode", () => {
describe("resolveInitialPermissionMode", () => {
const logger = () => ({ error: vi.fn() });
const initialMode = (
sources: Parameters<typeof resolveInitialPermissionMode>[0],
log: { error: (...args: unknown[]) => void },
allowBypass: boolean,
allowAuto = true,
) => resolveInitialPermissionMode(sources, log, { allowBypass, allowAuto }).mode;
it.each([
{ allowAuto: true, expected: "auto" },
{ allowAuto: false, expected: "default" },
])(
"starts in $expected without defaultMode when allowAuto is $allowAuto",
({ allowAuto, expected }) => {
expect(resolveInitialPermissionMode({}, logger(), { allowBypass: true, allowAuto })).toEqual({
mode: expected,
builtInDefault: true,
});
},
);
it("falls back to defaultMode for a resumed auto when Auto is disabled", () => {
expect(initialMode({ resumed: "auto", defaultMode: "plan" }, logger(), true, false)).toBe(
"plan",
);
});
it("uses defaultMode when nothing else is given", () => {
expect(resolveInitialPermissionMode({ defaultMode: "plan" }, logger(), true)).toBe("plan");
expect(initialMode({ defaultMode: "plan" }, logger(), true)).toBe("plan");
});
it("continues in the mode of the resumed transcript instead of defaultMode", () => {
expect(
resolveInitialPermissionMode(
{ resumed: "bypassPermissions", defaultMode: "plan" },
logger(),
true,
),
).toBe("bypassPermissions");
});
it.each(["default", "acceptEdits", "plan", "auto", "bypassPermissions"])(
"continues in the supported restored mode %s instead of defaultMode",
(resumed) => {
expect(initialMode({ resumed, defaultMode: "dontAsk" }, logger(), true)).toBe(resumed);
},
);
it("lets an explicit mode win over the resumed transcript", () => {
expect(
resolveInitialPermissionMode(
initialMode(
{ explicit: "acceptEdits", resumed: "bypassPermissions", defaultMode: "plan" },
logger(),
true,
@@ -275,19 +317,31 @@ describe("resolveInitialPermissionMode", () => {
it("falls back to defaultMode when bypass is not available now", () => {
const log = logger();
expect(
resolveInitialPermissionMode(
{ resumed: "bypassPermissions", defaultMode: "plan" },
log,
false,
),
).toBe("plan");
expect(initialMode({ resumed: "bypassPermissions", defaultMode: "plan" }, log, false)).toBe(
"plan",
);
expect(log.error).toHaveBeenCalledWith(expect.stringContaining('"bypassPermissions"'));
});
it("falls back to defaultMode for an unknown resumed mode", () => {
expect(
resolveInitialPermissionMode({ resumed: "warp", defaultMode: "acceptEdits" }, logger(), true),
).toBe("acceptEdits");
it.each(["warp", "dontAsk"])("falls back to defaultMode for resumed mode %s", (resumed) => {
expect(initialMode({ resumed, defaultMode: "acceptEdits" }, logger(), true)).toBe(
"acceptEdits",
);
});
it.each([
{ defaultMode: "plan", allowBypass: true, expected: "plan" },
{ defaultMode: "bypassPermissions", allowBypass: true, expected: "bypassPermissions" },
{ defaultMode: "bypassPermissions", allowBypass: false, expected: "default" },
{ defaultMode: "dontAsk", allowBypass: true, expected: "default" },
{ defaultMode: "warp", allowBypass: true, expected: "default" },
{ defaultMode: undefined, allowBypass: true, expected: "auto" },
])(
"uses available settings fallback $defaultMode for an unavailable restored mode",
({ defaultMode, allowBypass, expected }) => {
expect(initialMode({ resumed: "dontAsk", defaultMode }, logger(), allowBypass)).toBe(
expected,
);
},
);
});

View File

@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import {
clientSupportsNotices,
collapseHookSpeaker,
MAX_NOTICE_TITLE_LENGTH,
normalizeNoticeText,
noticeOrTranscriptUpdate,
@@ -8,6 +9,7 @@ import {
noticeTranscriptText,
sentenceCase,
splitNoticeText,
transcriptBlock,
} from "../session-notices.js";
describe("session notices", () => {
@@ -30,21 +32,42 @@ describe("session notices", () => {
expect(noticeTranscriptText({ severity: "info", title: "Done" })).toBe("**Done**");
expect(noticeOrTranscriptUpdate(notice, false)).toEqual({
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: "**Fast mode turned off:** Why." },
content: { type: "text", text: "\n\n> **Fast mode turned off:** Why.\n\n" },
});
expect(noticeOrTranscriptUpdate(notice, false, "custom line")).toEqual({
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: "custom line" },
content: { type: "text", text: "\n\n> custom line\n\n" },
});
const meta = { claudeCode: { kind: "informational", level: "warning" } };
expect(noticeOrTranscriptUpdate(notice, false, "custom line", meta)).toEqual({
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: "custom line" },
content: { type: "text", text: "\n\n> custom line\n\n" },
_meta: meta,
});
expect(noticeOrTranscriptUpdate(notice, true, "custom line", meta)).not.toHaveProperty("_meta");
});
it("puts a transcript notice in its own blockquote paragraph", () => {
// Without blank lines the chunk renders glued to Claude's last sentence:
// "…all done.**Notice:** Stop says: …".
expect(transcriptBlock("**Notice:** one line")).toBe("\n\n> **Notice:** one line\n\n");
expect(transcriptBlock("**Notice:** first\nsecond\n\nfourth\n")).toBe(
"\n\n> **Notice:** first\n> second\n>\n> fourth\n\n",
);
});
it("keeps a hook's repeated speaker prefix on the first line only", () => {
expect(collapseHookSpeaker("Stop says: token: 3 calls\nStop says: hand off now")).toBe(
"Stop says: token: 3 calls\nhand off now",
);
expect(collapseHookSpeaker("Stop says: one line")).toBe("Stop says: one line");
// Left alone unless every line repeats the same prefix.
expect(collapseHookSpeaker("Stop says: a\nsomething else")).toBe(
"Stop says: a\nsomething else",
);
expect(collapseHookSpeaker("no prefix\nStop says: b")).toBe("no prefix\nStop says: b");
});
it("sends a notice update when the client can present one", () => {
expect(
noticeOrTranscriptUpdate({ severity: "info", title: "Task stopped by user" }, true),

View File

@@ -2739,6 +2739,19 @@ describe("applyTaskCreate / applyTaskUpdate", () => {
});
});
it("takes the subject and description from the output, which holds the task as saved", () => {
const state: TaskState = new Map();
applyTaskCreate(
state,
{ subject: "Write tsets", description: "Cover Task*", activeForm: "Writing tests" },
{ task: { id: "1", subject: "Write tests", description: "Cover Task* flow" } },
);
expect(state.get("1")).toMatchObject({
subject: "Write tests",
description: "Cover Task* flow",
});
});
it("is a no-op when the output has no task ID", () => {
const state: TaskState = new Map();
applyTaskCreate(state, { subject: "X", description: "Y" }, undefined);

View File

@@ -278,11 +278,14 @@ export function applyTaskCreate(
): void {
const taskId = output?.task?.id;
if (!taskId || !input) return;
// The output holds the task as saved, which differs from the input when
// Claude Code corrects a malformed call or a PreToolUse hook changes it.
// `description` is in outputs since CLI 2.1.317.
state.set(taskId, {
subject: input.subject,
subject: output.task.subject ?? input.subject,
status: "pending",
activeForm: input.activeForm,
description: input.description,
description: output.task.description ?? input.description,
});
}