Compare commits

..

59 Commits

Author SHA1 Message Date
Ben Brandt
8efe828c76 feat: link the skills of --add-dir roots to their SKILL.md for AIR clients 2026-10-23 21:41:39 +02:00
Ben Brandt
0ad3d2063b fix: don't link a scoped skill to the unscoped skill of the same name 2026-10-23 21:35:53 +02:00
dependabot[bot]
0fe22ae5a1 fix: bump fast-uri from 3.1.8 to 3.1.9 (#1410)
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.8 to
3.1.9.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/fastify/fast-uri/releases">fast-uri's
releases</a>.</em></p>
<blockquote>
<h2>v3.1.9</h2>
<h2>Security</h2>
<p>This security release fixes four URI parsing and normalization
vulnerabilities in fast-uri 3.x. Applications that process untrusted URI
input should upgrade to v3.1.9.</p>
<ul>
<li><strong>Medium — URI scheme spoofing through percent-encoded
schemes</strong> (<a
href="https://github.com/fastify/fast-uri/security/advisories/GHSA-w74x-v6f9-j3xg">GHSA-w74x-v6f9-j3xg</a>,
CVE-2026-110306): percent-encoded scheme input is now rejected instead
of being decoded into a live scheme.</li>
<li><strong>Medium — Host confusion during domain-host
canonicalization</strong> (<a
href="https://github.com/fastify/fast-uri/security/advisories/GHSA-wjr9-j795-jxcr">GHSA-wjr9-j795-jxcr</a>,
CVE-2026-110180): malformed domain hosts containing ambiguous delimiters
or stripped control characters are now rejected instead of being
silently rewritten.</li>
<li><strong>Low — Incorrect relative-reference normalization</strong>
(<a
href="https://github.com/fastify/fast-uri/security/advisories/GHSA-xhq7-xmqj-769h">GHSA-xhq7-xmqj-769h</a>,
CVE-2026-110095): dot-segment removal now preserves the resolution
semantics of relative references.</li>
<li><strong>Low — Scheme injection in suffix references</strong> (<a
href="https://github.com/fastify/fast-uri/security/advisories/GHSA-333x-w4r6-6crp">GHSA-333x-w4r6-6crp</a>,
CVE-2026-110725): first-segment colons in path-only suffix references
are now escaped so the output is not reparsed as a scheme.</li>
</ul>
<p>Upgrade with:</p>
<pre lang="sh"><code>npm install fast-uri@3.1.9
</code></pre>
<p>Thanks to <a href="https://github.com/zekth"><code>@​zekth</code></a>
for reporting the scheme, host, and relative-reference issues, and to <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
for finding the suffix-reference issue and reviewing the
remediations.</p>
<h2>What's Changed</h2>
<ul>
<li>ci: enable trusted publishing releases (v3.x) by <a
href="https://github.com/mcollina"><code>@​mcollina</code></a> in <a
href="https://redirect.github.com/fastify/fast-uri/pull/229">fastify/fast-uri#229</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/fastify/fast-uri/compare/v3.1.8...v3.1.9">https://github.com/fastify/fast-uri/compare/v3.1.8...v3.1.9</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="0def70338a"><code>0def703</code></a>
Bumped v3.1.9</li>
<li><a
href="a1d0da2f5c"><code>a1d0da2</code></a>
test: generate only relative path references</li>
<li><a
href="9eb8759a98"><code>9eb8759</code></a>
fix: preserve relative paths in suffix and relative resolution</li>
<li><a
href="54c30f7589"><code>54c30f7</code></a>
docs: note suffix-reference authority ambiguity</li>
<li><a
href="39e6493c13"><code>39e6493</code></a>
fix: escape a first-segment colon in suffix references</li>
<li><a
href="745183c1d9"><code>745183c</code></a>
test: cover first-segment colon in suffix references</li>
<li><a
href="ddfc6a5112"><code>ddfc6a5</code></a>
fix(SEC-03): reject non reg-name characters in domain hosts</li>
<li><a
href="d86a8d29f4"><code>d86a8d2</code></a>
fix: keep a colon-first segment behind &quot;./&quot; in relative
normalization</li>
<li><a
href="eb483b5da2"><code>eb483b5</code></a>
test: cover colon-first relative references in normalization</li>
<li><a
href="b110a3571c"><code>b110a35</code></a>
fix(SEC-02): preserve meaning of relative references during
normalization</li>
<li>Additional commits viewable in <a
href="https://github.com/fastify/fast-uri/compare/v3.1.8...v3.1.9">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for fast-uri since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=fast-uri&package-manager=npm_and_yarn&previous-version=3.1.8&new-version=3.1.9)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-23 20:54:32 +02:00
Ben Brandt
59e292e29f feat: report RFD delegations as linked session messages (#1405)
## Summary

For a client on the subagents RFD (every ACP v2 client, and v1 clients
that declare `subagents`), session messages now replace the Agent/Task
call that launches a child. The parent no longer sees that tool call.
AIR's form is unchanged.

- **Prompt.** The launch's prompt is a `session_message` in both
transcripts: the parent's outgoing view and the child's incoming one.
Both use the launch's tool use id, and each names the other with
`recipientMessageId` / `senderMessageId` from
agentclientprotocol/agent-client-protocol#2465.
- **Answer.** A foreground child's answer, from the launch's result, is
the child's `session_message` to the parent (`<launch id>:reply`). It
uses the result as `AgentReporter` renders it, so the model-facing
`agentId: …` line and `<usage>` block are gone. A failure or
cancellation comes from the child's `idle` stop reason, which it already
reported. A background launch's result gets no reply.
- **Hand-back mode.** Claude Code (seen with 2.1.303) can have the child
hand its report back with a `SubagentHandback` call; the Agent result
then only says that the report "was delivered to you as a message". That
call is reported as the child's message to the parent, in both
transcripts and linked the same way, and the launch's reply is skipped
for that run. A failed hand-back delivered nothing, so it stays a tool
call of the child.
- **Unexposed work.** A launch whose child is never exposed is shown as
a tool call after all, with its final status
(`unexposedControlFallback`, the former `failedControlFallback`).
- **Hidden launches by id.** The adapter's own report that closes an
unfinished call (for example at a forced cancel) carries no `toolName`,
so the runtime tracks hidden launches by id to keep that report from
reaching the client for a call it never saw.
- **Replay** sends the same messages from the child's transcript: the
launch's prompt, the reply, and hand-backs. A launch without Claude
Code's metadata file keeps its tool call and the missing-history notice.
Replay can't tell a child's runs apart, so any hand-back in its
transcript suppresses its launch's reply.
- **SendMessage** calls stay tool calls of their sender, and the child
gets the incoming view without references, as before.
- README's subagent section describes the new behavior.

Do not merge before agentclientprotocol/agent-client-protocol#2465
settles the field names: a merge to `main` publishes a preview right
away. Clients on the published schema ignore the two fields (no
`deny_unknown_fields` in the Rust schema; the scenario validation with
the SDK's zod schema passes).

## Testing

- `npm run build`, `npm run check` and `npm run test:run` pass (57
files, 4 skipped as before).
- The six v2 `subagent-*.jsonl` scenario snapshots changed: the launch's
tool-call updates are gone, the parent gets the outgoing view, the
child's view carries `senderMessageId`, and foreground results became
`:reply` messages. AIR snapshots are unchanged.
- New unit tests cover hidden launches, nested launches, replies, failed
and background launches, unexposed launches with both final statuses,
the close report without a tool name, and hand-back success and failure,
live and on replay.
- A real run: the built adapter driven over v2 stdio with Claude Code,
one foreground subagent asked to answer PONG, then `session/resume` with
replay in a fresh process. Live and replay both showed the linked prompt
(parent → child), the linked hand-back "PONG" (child → parent), and no
Agent tool call.
2026-10-23 13:12:33 +02:00
Ben Brandt
50cd76e78d fix: publish the session title as soon as the CLI has it (#1406)
## Summary

Session titles showed up only after the first turn ended. With
background subagents, that could be minutes or hours later. Now the
adapter asks for the title when the prompt is sent to the CLI. In a live
run the title arrived **0.8s** after the prompt, compared with **17.9s**
(at turn end) on `main`.

## Why

- **The CLI already titles SDK sessions.** The bundled CLI (2.1.303)
titles a new session from its first prompt within seconds and stores it
as an `ai-title` transcript entry. `getSessionInfo` returns it as
`customTitle`. The header comment in `session-titles.ts` said the CLI
never titles SDK sessions; that is out of date. The CLI's off switch
only applies to the `sdk-cli` entrypoint without
`CLAUDE_AGENT_SDK_VERSION`, and the TS SDK sets `sdk-ts` and that
variable.
- **No message carries an AI title.** The CLI's internal
`system/session_title_changed` covers only `/rename`-style names, and
its schema says so.
- **The adapter only checked at `idle`.** `SessionTitles.onTurnEnd` ran
at `session_state_changed: idle`, which comes after the whole turn.
Since CLI 2.1.270 it also waits for background subagents to finish. In
the common case, `onTurnEnd` just picked up the title that had been on
disk for the whole turn.

## Change

- **New `SessionTitles.onPromptQueued`**, called right after the prompt
is pushed to the SDK. Until the session has a title, it:
1. reads the session file, and publishes a stored title if there is one
(a resumed session, or a `/rename`);
2. otherwise sends `generate_session_title` with the prompt text only,
in the background, and publishes the result.
- **No second model call.** The CLI runs this request alongside the turn
instead of queuing it behind it. Its handler returns the stored custom
title, then the AI title, and otherwise waits for the CLI's own title
call already in flight.
- **One try per prompt, like the CLI.** After a prompt-time request, the
turn end does not ask again. If nothing came back, it publishes the
stored summary as before, and the next prompt tries again.
- **Unchanged:**
- the turn end still titles a prompt too short to title from (under 10
characters, like "hi") from the prompt plus the reply;
  - the turn end still picks up a later `/rename`;
  - a title the client set still wins.

## Verification

- `npm run check` and `npm run test:run` pass (3275 passed, 30 skipped).
- New tests:
  - the title is published while the first turn is still running;
- the raw prompt is not published at turn end while a title is still
pending;
- a short "hi" prompt is titled at turn end from the prompt and the
reply.
- The latch-release test now pins one request per prompt. Removing the
new call site makes two of the tests fail.
- Live check: one real text-only turn against this branch and against
`main`, both on the bundled CLI 2.1.303, timing `session_info_update`
against the prompt response:

  | build | title published | prompt response |
  | --- | --- | --- |
  | this branch | 0.8s | 17.1s |
  | `main` | 17.9s | 17.9s |

Both runs got the same title, and both transcripts contain the same two
`ai-title` entries. So the change adds no title generation; the CLI
itself writes the entry twice.

## Risks

- **Undocumented CLI behaviour.** This depends on the CLI answering
`generate_session_title` mid-turn and returning the title it is already
generating, which `sdk.d.ts` does not document. If a future CLI changes
that, the worst case is one extra small-model call per session. The
latch still limits it to one success.
- **Titles now arrive mid-turn.** Clients that expected
`session_info_update` only between turns should be checked.
- **Steering is not covered.** Messages injected with
`_session/steering` don't trigger a title request.
2026-10-23 11:08:18 +00:00
Ben Brandt
aa62f44886 ci: resolve latest Node LTS patches before using runner cache (#1404)
## Summary
- Enable `check-latest: true` for all three Node setup steps: CI, stable
publishing and preview publishing.
- Preserve the existing `lts/*` policy, action pins, npm settings,
package version and `engines.node` requirement.

## Why
The latest sampled
[CI](https://github.com/agentclientprotocol/claude-agent-acp/actions/runs/39865544394)
and [preview
publishing](https://github.com/agentclientprotocol/claude-agent-acp/actions/runs/39865544489)
runs both selected cached Node **24.21.0**. That predates **24.22.1**,
the fixed Node 24 release for the [October 21 security
updates](https://nodejs.org/en/blog/vulnerability/october-2026-security-releases),
including CVE-2026-111497.

## Validation
The same checks passed before and after:
- Baseline on local Node **24.22.0**: `npm ci && npm run check && npm
run build && npm run test:run`.
- After on official Node **24.22.1** (archive SHA-256 verified): `npm ci
&& npm run check && npm run build && npm run test:run`.
- Both runs: **3,272 passed, 30 skipped**; the skipped/integration
coverage remains unverified.
- Parsed workflow configuration to verify all three setup steps keep
`lts/*` and enable `check-latest`.
- Tests ran natively on macOS; GitHub runner selection remains to be
confirmed by PR CI. No publish or release commands were run.

## Risk / rollout
Low-risk CI-only runtime freshness change. Verify the next
CI/stable/preview job's setup-node log resolves at least **24.22.1** for
Node 24. Reverting restores cache-first selection, which can restore an
unpatched runtime; it is not a mitigation.

This patches the build/publishing environment, not end users' separately
installed Node runtimes. No application-code or release-version changes
are included.
2026-10-23 11:19:16 +02:00
nikita-ashihmin
9b3a5c891c feat: end a turn at its result for AIR clients with backgroundSubagents (#1262)
## What

This PR adds the AIR client capability `backgroundSubagents`. A client
declares it in
`initialize.clientCapabilities._meta.jetbrains.air.capabilities`.

With the capability, an ACP v1 prompt turn ends at its result. This is
also true while background subagents of the turn still run, including a
subagent that SendMessage resumed. Subagent updates, permission requests
and elicitations then go out after the prompt response. Their routing
does not change. The agent advertises the capability only to ACP v1 AIR
clients.

## Why

The hold of #864/#866 keeps `session/prompt` open until the subagents
finish. The Claude CLI ends the model turn at `result`, and only `idle`
waits for the background agents. In AIR the hold hides the end of the
answer, and the next user message steers into an old turn. One
SendMessage-resumed subagent kept a turn open for about two hours.

## Unchanged

- A client without the capability keeps the hold exactly. So #866 stays
fixed for clients that stop listening at the prompt response.
- ACP v2 keeps the hold too.

## Related changes

- Out of a turn, the ExitPlanMode request of such a client does not
offer the clear-context option. The fresh query must continue an ACP
turn, and there is none.
- An ExitPlanMode interruption of an autonomous cycle with no held turn
now clears its markers.

## Docs

`docs/air-extensions.md` has a new "Background subagents" section.

## Tests

New capability cases:

- a fresh subagent;
- a SendMessage-resumed subagent;
- a permission request after the prompt response, with the
`parentToolUseId` attribution;
- an autonomous followup that does not end the queued next prompt;
- ExitPlanMode out of a turn, with and without a reset;
- the exclusion of a running subagent from the incomplete-tool check.

The existing tests cover the path without the capability. When the gate
is disabled, 7 of the 8 new cases fail. The eighth is the in-turn
control case and passes in both configurations.

`npm run build`, `npm run check` and `npm run test:run` pass. The
exception is 4 `providers.test.ts` cases. They depend on the environment
(a local proxy `baseUrl`) and fail on `main` too.
2026-10-22 15:31:05 -07:00
dependabot[bot]
ecaa2c935b fix: bump postcss from 8.5.30 to 8.5.31 in the minor group (#1403)
Bumps the minor group with 1 update:
[postcss](https://github.com/postcss/postcss).

Updates `postcss` from 8.5.30 to 8.5.31
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/releases">postcss's
releases</a>.</em></p>
<blockquote>
<h2>8.5.31</h2>
<ul>
<li>Fix better source map on <code>&lt;/style&gt;</code> escaping (by <a
href="https://github.com/firefoxic"><code>@​firefoxic</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/blob/main/CHANGELOG.md">postcss's
changelog</a>.</em></p>
<blockquote>
<h2>8.5.31</h2>
<ul>
<li>Fix better source map on <code>&lt;/style&gt;</code> escaping (by <a
href="https://github.com/firefoxic"><code>@​firefoxic</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="d1908579d6"><code>d190857</code></a>
Release 8.5.31 version</li>
<li><a
href="4ef6d8cc03"><code>4ef6d8c</code></a>
Update dependencies</li>
<li><a
href="6abd674231"><code>6abd674</code></a>
Call the builder with every node when the output is escaped (<a
href="https://redirect.github.com/postcss/postcss/issues/2175">#2175</a>)</li>
<li>See full diff in <a
href="https://github.com/postcss/postcss/compare/8.5.30...8.5.31">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=postcss&package-manager=npm_and_yarn&previous-version=8.5.30&new-version=8.5.31)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-22 16:38:37 +02:00
Charles McLaughlin
5b11f470b1 feat: show messages typed in other Remote Control clients (#1371)
A message typed in claude.ai or the Claude mobile app while Remote
Control is on never appeared in the client that hosts the session, even
though Claude's reply to it did. This makes both sides of that
conversation show up.

Two causes, one commit each:

- **Messages from other clients were dropped.** Claude Code echoes them
to the adapter as a replayed user message with `origin: { kind: "human"
}` and a uuid the adapter never created. That matched no turn, so it
fell into the "unrelated replay" branch and was ignored. Human-origin,
top-level, non-synthetic messages that match no turn are now forwarded
as `user_message_chunk` (mapped to `user_message` on ACP v2). The echoes
of the adapter's own prompts still match their turn first and stay out
of the feed.
- **The stream reader was not running after `/remote-control`.** The
reader that forwards Claude Code's output starts with the first prompt,
and `/remote-control` answers without a turn. In a thread where it was
the first thing run, nothing read the stream, so neither the phone's
message nor Claude's reply reached the client. The command now starts
the reader. This also fixes the reply side for #1197.

## Testing

- `npm run check`, `npm run build` and `npm run test:run` pass, with new
tests in `remote-user-messages.test.ts` and `remote-control.test.ts`.
The tests for both fixes fail without them.
- Ran the built adapter as a custom agent in Zed: after
`/remote-control`, a message sent from the Claude iOS app showed up in
the Zed thread along with Claude's reply.
- Not tested: messages with images or attachments, and whether resuming
a session can replay old human messages through the same path.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 18:05:56 +00:00
Ilya Kirillov
e9f986b8a9 fix: IJAI-3744 keep a session running while its background tasks are open (#1399)
## Summary

A Claude Code session can start a background shell command or a
background sub-agent, and then end its turn. When a task ends, Claude
Code wakes the session for a short turn. The session index reported
`idle` at each turn end, so a client saw one completion for each task.

Now the index finds the open background tasks in the transcript. A turn
end with an open task stays `running`, also after 10 minutes. The
session gets one `idle`, when the last task ends. The rules are the same
as in the native Claude provider of AIR (IJAI-3744).

## Rules

- A task opens with a tool result that has `backgroundTaskId`, or
`isAsync` with an `agentId`.
- A task closes with its `<task-notification>` (a user record or a queue
`remove`), or with a `TaskStop` result.
- The wait ends when no live process holds the session, or when the
registry status is `idle`.
- A task can start before the tail window. A later scan applies the tail
on top of the earlier task set. When it cannot, the index reads the
whole transcript, but only for a finished turn of a live session.

## Not in this PR

- A background task that a sub-agent starts. Its start record is only in
the transcript of the sub-agent.
- A session that this connection runs. Its state comes from the SDK.

## Tests

- `background-tasks.test.ts`: the record orders of the native provider
tests, in a small transcript and in one longer than the tail window. The
tests also cover a task before the tail window, the end of the wait, and
the count of whole-file reads.
- A real check with Claude Code 2.1.306 in a terminal. With two
background shells or two background sub-agents, `main` gave three `idle`
and this change gave one.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 15:27:16 +02:00
Ben Brandt
4d378f93f3 feat: serve experimental ACP v2 (#1400)
## Summary

The draft ACP v2 is no longer opt-in. The adapter always connects
through the SDK's protocol router (as codex-acp does), so a client that
asks for protocol version 2 or later in `initialize` gets v2, and every
other client gets v1 as before. v2 is still experimental and follows the
draft as it changes.

- Removes `CLAUDE_AGENT_ACP_EXPERIMENTAL_V2`, which only turned on what
is now the default.
- Adds `CLAUDE_AGENT_ACP_PROTOCOL` to limit the adapter to one version:
  - unset, empty, or `auto`: both, picked by the client's `initialize`.
- `v1`: a client that asks for v2 is negotiated down to v1. This is the
escape hatch if a client's v2 handling breaks.
- `v2`: a v1 client's `initialize` is rejected with `-32022 Unsupported
protocol version`.
- Any other value fails at startup, so a typo like
`CLAUDE_AGENT_ACP_PROTOCOL=1` doesn't silently serve both.
- Fixes the comment in `src/acp-subagents.ts` that said v2 clients don't
get subagents.

Note: a client that already sends `protocolVersion >= 2` switches to the
v2 surface as soon as this lands on `main`, which publishes a preview.

## Testing

- New routing tests: down-negotiation to v1, rejecting v1 when limited
to v2, env parsing.
- `npm run check`, `npm run build`, `npm run test:run` pass.
- Ran the built binary over stdio with each setting: default → v2 for a
v2 client, `v1` → protocol version 1, `v2` → `-32022` for a v1 client,
invalid value → exits with an error at startup.

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 13:37:06 +02:00
Andrey Bragin
a1d0eeeaad fix: publish turns that Claude Code starts after end_turn as an async task in v1 (#1390)
## Motivation

Claude Code can start a turn of its own after a prompt has ended, for
example to answer the notification of a finished background task. ACP v1
has no session state, and the client already got `end_turn` for its last
prompt. The output of that turn just keeps arriving as session updates,
and the client has no signal that the agent is working again, or when it
stops.

## Change

The adapter now publishes such a turn as an async task (AIR async tasks
extension) with the id `autonomous-turn-<uuid>`, the name `Claude is
working`, `taskType: "turn"` and `showInTranscript: false`. Its
description is the summary of the last task notification, when there is
one.

- **Start:** the SDK moves to `running` while no prompt is active or
queued. From CLI 2.1.270 the state stays `running` while background
agents run, so the task also starts at the first root output with no
prompt open.
- **End:** the task ends `completed` at the turn's result or at `idle`,
and `failed` at an error result. A coalesced placeholder result
(`num_turns: 0`) does not end it.
- **Folded prompt:** a prompt that the turn takes in ends the task,
because that prompt owns the turn now.
- **Stop:** `_session/async_task/stop` on this task interrupts the turn,
since the SDK has no task to stop. It returns `{ "stopped": false }`
while a prompt waits for its turn, because the interrupt would drop that
prompt.
- **Lifecycle:** the SDK task list does not end the task, because the
SDK does not know about it. Shutdown fails it like other open tasks.

Clients without the async tasks capability get nothing new. The behavior
is documented in `docs/air-extensions.md` under "Autonomous turn".

## Tests

`acp-agent.test.ts` covers the start at `running` and at the first
output, the end at the result, the folded prompt, stop as an interrupt,
and a client without async tasks. `async-tasks.test.ts` covers the SDK
task list reconciliation and shutdown.
2026-10-20 15:24:42 +02:00
acp-release-bot[bot]
7bcbfc7a5c chore(main): release 0.94.0 (#1381)
🤖 I have created a release *beep* *boop*
---


##
[0.94.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.93.0...v0.94.0)
(2026-10-19)


### Features

* send one end event after the session/load history replay for AIR
([#1361](https://github.com/agentclientprotocol/claude-agent-acp/issues/1361))
([1420994](1420994ced))


### Bug Fixes

* match OpenRouter model ids in the gateway model listing
([#1365](https://github.com/agentclientprotocol/claude-agent-acp/issues/1365))
([bcf74c2](bcf74c2fe8))
* resolve a skill's SKILL.md by its declared name, not by its directory
name
([#1372](https://github.com/agentclientprotocol/claude-agent-acp/issues/1372))
([424db87](424db87535))
* update to ACP SDK 2.0.0
([#1386](https://github.com/agentclientprotocol/claude-agent-acp/issues/1386))
([ff01e06](ff01e06579))
* update to Claude Code 2.1.303
([#1384](https://github.com/agentclientprotocol/claude-agent-acp/issues/1384))
([2dab922](2dab922644))

---
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-10-19 21:29:13 +00:00
dependabot[bot]
2dab922644 fix: update to Claude Code 2.1.303 (#1384)
Updates `@anthropic-ai/claude-agent-sdk` from 0.3.300 to **0.3.303**
(Claude Code **2.1.303**), along with dependabot's minor-group bumps
(`@anthropic-ai/sdk`, eslint, vitest and transitive packages). 0.3.304
is only on the `next` dist-tag, so it is not included.

## Fixes for changes in the new SDK

The Agent SDK turns on fork subagents (`subagent_type: "fork"`) by
default in SDK sessions (0.3.302). Before this, ACP sessions never got
them. Live streaming already handles forks. History replay did not:

- **Replay showed the parent's launching message inside the fork.** A
fork's transcript starts with a copy of the parent's launching assistant
message (with the same API message id and every `tool_use`), then a user
message with a `"Fork started — processing in background"` result per
`tool_use` and a `<fork-boilerplate>…Your directive: <prompt>` text. On
`session/load`, the parent's text, tool calls and those results appeared
in the fork, and the fork's prompt showed the full boilerplate.
Replaying the copy also consumed the parent's tool-use cache entries, so
the parent's real Agent/parallel tool results were dropped. In the AIR
form, sibling forks were re-registered under the fork.
`readSubagentMessages` now drops everything before the directive message
and reduces it to the prompt (`stripForkPrefix`).
- **Lineage scan** (`subagentLineage`) ignores launch calls in a fork's
copied prefix, so it no longer attributes the parent's launches to the
fork.
- **Agent title** no longer shows `(high effort)` for a fork: a fork
ignores `effort` and runs at the parent's effort.

## Changelog review: nothing else to change

- **0.3.302: an omitted `permissionMode` now starts in `default` on
third-party providers.** The agent always passes `permissionMode`
explicitly, so this does not apply.
- **0.3.301: a full-disk login refresh now reports `invalid_request`
instead of `authentication_failed`/401.** This now maps to `bad_request`
rather than `auth_required`. That's the right outcome, since `/login`
cannot fix a full disk. Its text has no "Please run /login", so the
synthetic-login check does not catch it.
- **0.3.301: `setModel()` no longer fails at the usage limit.** This is
a free fix for model switches.
- **0.3.302: structured output refusals are now error results.** These
are covered by the existing result handling.
- **New APIs not adopted:**
  - Per-tool `permission_policy` on sdk MCP servers: we register none.
  - `reloadPlugins({ addPlugins })`: there's no plugin reload surface.
- `BUILTIN_TOOL_NAMES`/`LEGACY_TOOL_NAME_ALIASES`: `@alpha`, and nothing
here needs them.
  - MCP `_meta.ui`: unused.

Verified: `npm run build`, `npm run check`, `npm run test:run` (3205
passed). New tests cover the fork replay trace (RFD form),
`stripForkPrefix`, the lineage scan with a fork transcript (it fails
without the fix), and the fork title.

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 23:26:23 +02:00
Ben Brandt
ff01e06579 fix: update to ACP SDK 2.0.0 (#1386)
Updates `@agentclientprotocol/sdk` from 1.9.0 to 2.0.0.

SDK 2.0.0 makes the default entrypoints stable-only and removes the
legacy connection classes ([migration
guide](https://github.com/agentclientprotocol/typescript-sdk/blob/main/MIGRATION_STABLE_UNSTABLE.md)).

- Protocol imports move to `@agentclientprotocol/sdk/unstable` and
`@agentclientprotocol/sdk/experimental/v2/unstable`. The agent uses
unstable features (session forking, `systemPrompt`, turn usage,
subagents, v2 draft extras), so this keeps the same protocol surface as
1.9.0. No wire changes.
- The scenario schema validator loads `schema/v1/schema.unstable.json`,
because the unversioned `schema/schema.json` export was removed.
- `remote-control.test.ts` uses the repo's own `AcpClient` type instead
of the removed `AgentSideConnection`.
- Runtime code already used the `agent()` app API, so it needed no
migration.

Verified: `npm run build`, `npm run test:run` (3199 passed), `npm run
check`, and a separate typecheck of `examples/`.

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:01:53 +00:00
Charles McLaughlin
e155ecb419 test: expect the v2 state snapshot after the replay end event (#1385)
`main` has failed CI since #1361, on one test: `ACP v2 prompts > ends a
resume with replay with one end event for a client with loadReplayEnd`.

Two PRs that merged separately collided:

- #1366 made a v2 resume send a `state_update` snapshot after the replay
and before the response ("v2 requires a snapshot of the foreground state
after the replay", in `src/v2/agent.ts`).
- #1361 added the `loadReplayEnd` end event and this test, which
expected the end event to be the last update of the response. The test
predates the snapshot, so each PR passed CI on its own.

The snapshot is required by the v2 spec, so the code is right and the
test is stale. The test now expects the end event followed by the
snapshot, and still checks that the end event is sent once. Only
`src/tests/acp-v2.test.ts` changes.

## Testing

- `npm run check`, `npm run build` and `npm run test:run` pass (3199
passed, 30 skipped).
- The same test fails on `main` at `bcf74c2` without this change.

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

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 22:12:23 +02:00
Andrey Efimov
bcf74c2fe8 fix: match OpenRouter model ids in the gateway model listing (#1365)
The model picker was empty behind OpenRouter: `normalizeModelId` did not
match `anthropic/claude-opus-5.5:batch[1m]` to `claude-opus-5-5`. It now
strips a leading `anthropic/` and a `:variant` suffix, and maps `5.5` to
`5-5`.
2026-10-19 08:33:33 +00:00
nikita-ashihmin
1420994ced feat: send one end event after the session/load history replay for AIR (#1361)
## What

A client that declares the AIR capability `loadReplayEnd` gets one extra
`session/update` after the history replay of `session/load`:

```json
{
  "sessionId": "<loaded session>",
  "update": {
    "sessionUpdate": "session_info_update",
    "_meta": { "jetbrains": { "air": { "version": 1, "loadReplayEnd": true } } }
  }
}
```

The client declares the capability in
`clientCapabilities._meta.jetbrains.air.capabilities`, as it declares
the other AIR extensions. The agent does not advertise it.

## Why

ACP has no signal that the history replay of `session/load` is over,
other than the response. This adapter replays the transcript while the
Claude Code CLI starts, and the CLI start takes most of the load time.
So the history is whole long before the response. With this event, the
client can show the history at once.

The protocol side is the RFD
https://github.com/agentclientprotocol/agent-client-protocol/pull/2407.

## Design change

The first version of this PR marked each replayed notification with an
index (`loadSessionReplayIndex`) and the last one with
`loadSessionEndOfRecordedHistory`. To know the last one, it held the
latest update and queued every other outbound message of the session
behind it. That design is replaced with a capability-gated single end
event. The replay sends as before. Only one event is added at its end.
There are no indexes, no held update and no queue.

## When the event is sent

- After every replayed update, the subagent history that the parent
replay sends to the child sessions included, and before the
`session/load` response.
- Also for an empty history.
- For `session/load` of a session that starts while the replay runs, for
`session/load` of a session that is already live, and for the ACP v2
`session/resume` with `replayFrom: {type: "start"}`.

It is not sent:

- When the replay stops (the CLI start failed during the replay) or
fails.
- For a live update.
- To a client without the capability.

The update has no `title` and no `updatedAt`. A `null` there would clear
the value. The load can still fail after the event, for example when the
CLI start fails after the replay. The event means that the recorded
history arrived whole.

## Client status

JetBrains Air shows the history before the load answer only after the
Kotlin SDK delivers the updates of a loading session before the answer.
An SDK PR for this is in progress.

## Tests

- `create-session-options.test.ts`: the event comes once after the
replay and before the response; before the CLI start finishes; for an
empty history; for an already live session; not when the CLI start fails
part way; not when the replay fails; not without the capability. The
event has no `title` and no `updatedAt` key.
- `acp-agent.test.ts`: the subagent history from its own transcript file
comes before the event.
- `acp-v2.test.ts`: v2 `session/resume` with a replay ends with the
event before the response; a resume without a replay sends none.
- `docs/air-extensions.md`: a new "Load replay end" section, and the
capability and key rows.
2026-10-19 08:05:46 +04:00
nikita-ashihmin
424db87535 fix: resolve a skill's SKILL.md by its declared name, not by its directory name (#1372)
## Problem

Claude Code advertises a skill by the `name:` of its `SKILL.md`
frontmatter, which does not have to match the name of the directory — or
of the symlink — that holds the file. `resolveSkillPath` joined the
advertised name into each container path and only checked whether the
result exists, so every such skill resolved to nothing.

For an AIR client the consequence is visible:
`getAvailableSlashCommands` attaches `_meta.jetbrains.air.kind =
"skill"` and the `SKILL.md` path only when the path resolves, so the
skill reached the client as a plain command and disappeared from the
skill completion, while `/name` kept working. Symlinked skills hit this
most often, because the link name is usually chosen independently of the
skill's declared name.

Reproduced with a project whose `.claude/skills/mismatch/SKILL.md`
declares `name: frontname`, and with `.claude/skills/aliased2 ->
…/target2` declaring `name: front2`: both were advertised without any
`_meta`. The same happens in a large repo where
`.claude/skills/rubymine` declares `name: rubymine-workflows`.

## Fix

The existing join-by-name probe stays the first attempt, so nothing
changes for the common layout. When it misses, the same containers are
scanned, in the same order, for a `SKILL.md` whose frontmatter declares
the requested name:

- `statSync`/`existsSync` follow links, so a skill behind a symlinked
directory is scanned like any other;
- each container is listed per lookup, and a `SKILL.md` whose
modification time has not changed keeps the name of the previous scan,
so an unchanged file is parsed once;
- only the frontmatter block of at most 8 KB is parsed, so a `name:` in
the body is not a name;
- a directory that is itself named like the skill still wins over
another skill that merely declares the name.

## Tests

Eight cases in `src/tests/tools.test.ts` cover a directory/declared-name
mismatch, a symlink with a differing link name in both `.claude/skills`
and `.agents/skills`, the precedence of the directory that carries the
name, scoped and plugin spellings resolved by declared name, a file
without frontmatter, and a declared name that changes after the first
resolution.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
2026-10-19 08:05:25 +04:00
acp-release-bot[bot]
dbf386d8d3 chore(main): release 0.93.0 (#1369)
🤖 I have created a release *beep* *boop*
---


##
[0.93.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.92.0...v0.93.0)
(2026-10-16)


### Features

* add /remote-control command to start Remote Control for a session
([#1197](https://github.com/agentclientprotocol/claude-agent-acp/issues/1197))
([db9f50f](db9f50f021))


### Bug Fixes

* hold a fork back from the session list until its session/fork response
is out
([#1368](https://github.com/agentclientprotocol/claude-agent-acp/issues/1368))
([f5967fd](f5967fd6b4))

---
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-10-16 21:19:20 +02:00
Charles McLaughlin
db9f50f021 feat: add /remote-control command to start Remote Control for a session (#1197)
Adds a `/remote-control [name]` command (alias `/rc`) so a session
hosted over ACP can be continued from claude.ai/code or the Claude
mobile app. Addresses #459.

The CLI's own `/remote-control` is terminal-bound and isn't advertised
to SDK hosts, and `remoteControlAtStartup` only applies to interactive
sessions. The SDK does expose a runtime `enableRemoteControl(enabled,
name?)` on the query object, which returns the session URL. See
anthropics/claude-agent-sdk-typescript#460.

- The adapter handles the command itself in `prompt()`, calls
`enableRemoteControl(true, name)`, and replies with the session URL as
an agent message. The turn ends without going to the model.
- Failures (for example Remote Control disabled by policy) are reported
to the user in the same way.
- The command is added to `available_commands_update` only when the
query exposes the method.
- `enableRemoteControl` is not in the published `Query` types yet, so it
is typed locally the same way as `mcpAuthenticate`.

## Testing

- `npm run check`, `npm run build` and `npm run test:run` pass,
including new tests in `src/tests/remote-control.test.ts`.
- Ran the built agent through `examples/simple-client.ts` against a real
Claude Max session: the command was advertised, `/remote-control`
returned a working `claude.ai/code` URL, and prompts sent from the
Claude iOS app ran in the session and were answered.

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

---------

Co-authored-by: Noah Eisenbruch <noaheisenbruch@gmail.com>
Co-authored-by: Andrey Bragin <andrey.bragin@jetbrains.com>
2026-10-16 21:07:25 +02:00
nikita-ashihmin
f5967fd6b4 fix: hold a fork back from the session list until its session/fork response is out (#1368)
## Problem

`session/fork` copies the transcript and then starts the forked session,
which takes several seconds. Meanwhile the list subscription sees the
new transcript and sends `_session/list/changes` with the fork row
before the fork response. The client meets an unknown session and lists
it as a session of its own. JetBrains Air then refuses the fork
(IJAI-4105).

## Fix

- `ForkHolds` (in `session-index/list-subscriptions.ts`, owned by the
session index service) holds the row of a fork until the `session/fork`
response is out:
  - by `forkedFrom`, for an unsent row, while the transcript is copied;
- by the fork id after the copy. This covers a fork whose first record
is over 64 KB, because the scan reads `forkedFrom` from the head only.
- `session/list` leaves a held fork out too.
- The v1 and v2 fork handlers run in `forking(...)`. A `setTimeout(0)`
releases the rows after the SDK has written the response.
- The holds live in the service, so a subscription that starts during a
fork holds that fork too.

## Tests

`holds a fork back until the response of its session/fork is out`
(session-index.test.ts). It fails without the fix.
2026-10-16 22:38:37 +04:00
acp-release-bot[bot]
af3d7f9335 chore(main): release 0.92.0 (#1358)
🤖 I have created a release *beep* *boop*
---


##
[0.92.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.91.0...v0.92.0)
(2026-10-16)


### Features

* add the MCP server and tool to AIR tool calls
([#1359](https://github.com/agentclientprotocol/claude-agent-acp/issues/1359))
([872e93f](872e93f11f))
* publish the predicted follow-up prompt as _meta.promptSuggestions
([#1356](https://github.com/agentclientprotocol/claude-agent-acp/issues/1356))
([78a29f5](78a29f58ba))
* update ACP SDK to 1.9.0 and adopt the v2 spec clarifications
([#1366](https://github.com/agentclientprotocol/claude-agent-acp/issues/1366))
([68bf74f](68bf74f58e))


### Bug Fixes

* include subagent file edits in the file-change report
([#1357](https://github.com/agentclientprotocol/claude-agent-acp/issues/1357))
([910d83c](910d83ceea))
* restore resumed subagents after an adapter restart
([#1364](https://github.com/agentclientprotocol/claude-agent-acp/issues/1364))
([26f144a](26f144a2a5))

---
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-10-16 14:39:40 +02:00
Ben Brandt
68bf74f58e feat: update ACP SDK to 1.9.0 and adopt the v2 spec clarifications (#1366)
Updates `@agentclientprotocol/sdk` to 1.9.0 (schema v1.26.0,
v2.0.0-alpha.9) and adapts the agent to the v2 spec clarifications in
that release.

## Changes

**Client-provided system prompt (unstable)**
- Advertises `sessionCapabilities.systemPrompt` with `mode.override` on
v1 and `session.systemPrompt` on v2.
- Accepted on `session/new`, `load`, `resume`, and `fork`:
  - `append` adds the text after the Claude Code preset.
- `override` replaces the preset with the text. Tools, MCP servers,
permissions, and settings (including CLAUDE.md through `settingSources`)
are unchanged.
- An unsupported mode or non-text content fails with `invalid_params`
before any session is created, reused, or forked.
- Takes precedence over the `_meta.systemPrompt` extension. AIR custom
instructions still follow the client text.
- A different prompt on resume recreates a live session. Claude Code
reuses its saved system prompt until the next compaction, which the RFD
allows.

**v2 session/resume state snapshot**
- Sends the current foreground state (`idle`, `running`, or
`requires_action`) after any replay and before the response, as the
clarified spec requires. This applies even without a replay.

**v2 replay of subagents**
- No longer replays a child's past `running`/`idle` as current activity.
Each replayed child gets one `state_update` with its last state after
the replay.

**v2 tool call `_meta` patches**
- Reports carry only the top-level keys that changed. `claudeCode` is
still sent whole, since clients don't merge nested objects.
- A removed key is sent as `null`.
- A tool call id that starts again removes the old keys.
- A replayed tool call this connection has not reported sends `_meta:
null` first, then the current keys.

**Cancellation and close**
- A `session/close` whose turns don't end within the teardown timeout
settles them as cancelled. The client gets `idle` with `cancelled`, or
`-32800` for a prompt never inserted, before the close response.
- A prompt racing `session/close` ends as cancelled (`-32800` on v2)
instead of `-32603`. On v1 it now returns `stopReason: cancelled`,
matching a prompt queued just before the close.
- A stream error after a cancel ends the work as `cancelled` instead of
`error`.

## Not in this PR
- After a URL-mode MCP elicitation is accepted, the session reports
`running` right away. The spec asks for `requires_action` until the
external interaction finishes, but a server may never send
`elicitation_complete`, so this needs its own design.
- `requires_action` is reported while one foreground subagent waits,
even if other foreground work can continue. This is SHOULD-level in the
spec.

## Testing
- `npm run build`, `npm run check`, and `npm run test:run` pass.
- Golden files changed because of the new capability and the resume
snapshot. `compare.ts` allows the new capability against origin/main.
- `session index cost > reads only the changed transcript for a
subscription` failed intermittently locally (about 3 in 15 runs) on a
loaded machine. This PR doesn't touch the session index, but watch it in
CI.

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:37:08 +02:00
Mark Tkachenko
26f144a2a5 fix: restore resumed subagents after an adapter restart (#1364)
After a restart the AIR subagent map is empty, so a SendMessage resume
opened no generation and the child's messages stayed buffered under the
old launch id. Restore the agent from the session history on a resume
signal or on a message with an unseen parent_tool_use_id, with a
generation id that can't collide with ids the client stored before.
2026-10-16 14:15:35 +02:00
Mark Tkachenko
872e93f11f feat: add the MCP server and tool to AIR tool calls (#1359)
AIR gets `_meta.jetbrains.air.mcp = {server, tool}` on MCP tool calls,
so the MCP card does not depend on the title format.

The adapter splits the `mcp__<server>__<tool>` tool name the way Claude
Code does. `server` is the normalized name. Clients other than AIR get
nothing new.

Split from IJAI-872. The SDK does not expose MCP progress yet, so this
PR does not forward it.
2026-10-15 17:04:17 +02:00
nikita-ashihmin
78a29f58ba feat: publish the predicted follow-up prompt as _meta.promptSuggestions (#1356)
## What

The Claude Code CLI can predict the prompt the user is likely to send
next. This adapter asked for nothing and dropped the `prompt_suggestion`
message. It now asks for the prediction (`promptSuggestions: true`) and
forwards the one prediction of a turn.

ACP has no field for a composer prefill, so the value travels as

```json
{
  "sessionUpdate": "session_info_update",
  "_meta": { "promptSuggestions": { "suggestions": [{ "text": "Run the tests" }] } }
}
```

The key names no vendor, so another client can read it. AIR already
renders the first non-blank text as gray text in an empty composer,
where Tab fills the composer and sends nothing.

## Behavior

- `suggestions: []` clears the prediction the client holds: at the next
`session/prompt`, because that turn's prediction is spent, and on a
`conversation_reset`, because the transcript it was drawn from is gone.
- A prediction the next prompt outran — the user sent the next prompt
before the adapter read the prediction off the SDK stream — is dropped
instead of published mid-turn.
- A blank prediction never goes on the wire.
- The update is sent only to an AIR client, like the other extension
keys, so non-AIR traffic is unchanged (the key is added to
`AIR_ONLY_META_KEYS`; the `origin/main` scenario baselines stay
byte-identical).
- The CLI predicts nothing on the first turn, after an API error, in
plan mode, and at the usage limit, so a turn without a prediction is
normal. A caller can opt out with
`_meta.claudeCode.options.promptSuggestions: false`.

Documented in `docs/air-extensions.md`.

## Tests

`src/tests/prompt-suggestions.test.ts` covers the wire shape, the
publish after the turn's result, the clear at the next prompt, the
dropped stale prediction, the blank prediction, and a non-AIR client.
`src/tests/create-session-options.test.ts` covers the SDK option and the
caller opt-out.

`npm run check`, `npm run build` and the touched suites are green; the
full `test:run` has pre-existing flakiness on this machine
(session-index watchers, providers) unrelated to this change.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
2026-10-15 17:55:44 +04:00
Mark Tkachenko
910d83ceea fix: include subagent file edits in the file-change report (#1357)
## Summary

`agentFileChangeReport` took its path list only from Claude's file
checkpoints (`rewindFiles` dry run). A checkpoint tracks only the files
that the root agent edits with its own file tool. So a file that only a
subagent edited never appeared in the report. Air then showed no "Since
last turn" row for it, and Last Turn review could not include it.

- Collect the `file_path` / `notebook_path` of successful
`Edit`/`Write`/`MultiEdit`/`NotebookEdit` results from subagent messages
in the stream. Store them on the active turn. This also covers
background subagents, because the turn stays open until they finish.
- Merge these paths with the checkpoint list. When the checkpoint
preview times out or is unavailable, report the subagent paths instead
of `unavailable`.
2026-10-15 15:39:18 +02:00
acp-release-bot[bot]
c0af592d25 chore(main): release 0.91.0 (#1333)
🤖 I have created a release *beep* *boop*
---


##
[0.91.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.90.0...v0.91.0)
(2026-10-15)


### Features

* keep background subagents running after a parent cancel, for clients
that can cancel them
([#1348](https://github.com/agentclientprotocol/claude-agent-acp/issues/1348))
([ba3c7be](ba3c7be05e))
* let a client cancel one subagent with session/cancel on its child
session
([#1347](https://github.com/agentclientprotocol/claude-agent-acp/issues/1347))
([aeb69e7](aeb69e7ed1))
* mark session list rows of worktrees Claude Code made (agentWorktree)
([#1352](https://github.com/agentclientprotocol/claude-agent-acp/issues/1352))
([1a1f1a0](1a1f1a0e80))
* narrow models to what the providers/set gateway lists
([#1340](https://github.com/agentclientprotocol/claude-agent-acp/issues/1340))
([e8eb9d3](e8eb9d340f))
* restore a subagent from before a reload when SendMessage resumes it,
for subagents RFD clients
([#1332](https://github.com/agentclientprotocol/claude-agent-acp/issues/1332))
([df21b3f](df21b3fb56))


### Bug Fixes

* close the tool calls of a background subagent that the SDK reports
stopped after a cancel
([#1336](https://github.com/agentclientprotocol/claude-agent-acp/issues/1336))
([9a1b90f](9a1b90fb9d))
* close the tool calls of a cycle that Claude Code started and a cancel
stopped
([#1339](https://github.com/agentclientprotocol/claude-agent-acp/issues/1339))
([1fd217f](1fd217f3e7))
* close the tool calls that a cycle Claude Code started abandoned, at
its end
([#1344](https://github.com/agentclientprotocol/claude-agent-acp/issues/1344))
([594aa8c](594aa8c955))
* deliver the output of a background subagent that a cancel did not stop
([#1345](https://github.com/agentclientprotocol/claude-agent-acp/issues/1345))
([bcc024d](bcc024d2c9))
* end the terminal of a command whose tool call is closed without a
result
([#1335](https://github.com/agentclientprotocol/claude-agent-acp/issues/1335))
([764f339](764f339c3e))
* end the tool calls of a cycle that Claude Code started and a cancel
stopped, on v1
([#1342](https://github.com/agentclientprotocol/claude-agent-acp/issues/1342))
([b4788d6](b4788d61ea))
* keep a live session that was never prompted when a resume or load
changes its settings
([#1353](https://github.com/agentclientprotocol/claude-agent-acp/issues/1353))
([0c8de13](0c8de1380f))
* let session/delete succeed for a session without a transcript
([#1351](https://github.com/agentclientprotocol/claude-agent-acp/issues/1351))
([fadbe18](fadbe1881c))
* report no running for the session's own request that a cancel
withdraws, on ACP v2
([#1346](https://github.com/agentclientprotocol/claude-agent-acp/issues/1346))
([d528647](d528647162))
* show nothing of a tool that cancelled work asks to run after the
cancel was reported
([#1334](https://github.com/agentclientprotocol/claude-agent-acp/issues/1334))
([ebf8b49](ebf8b49fd6))
* stop waiting for a question the client leaves open after Claude Code
cancels it
([#1338](https://github.com/agentclientprotocol/claude-agent-acp/issues/1338))
([c1e83b4](c1e83b46b7))
* update Claude Agent SDK to 0.3.300
([#1355](https://github.com/agentclientprotocol/claude-agent-acp/issues/1355))
([761b4c8](761b4c8359))

---
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-10-15 12:22:03 +00:00
Ben Brandt
761b4c8359 fix: update Claude Agent SDK to 0.3.300 (#1355)
Updates `@anthropic-ai/claude-agent-sdk` from 0.3.296 to 0.3.300 (parity
with Claude Code v2.1.300), and `@anthropic-ai/sdk` from 0.133.0 to
0.134.0.

No code changes needed. The new types are an optional `property_order`
on elicitation requests, `nextFireAt` on the CronCreate/CronList
outputs, and `owned` on Artifact outputs. We don't need any of them.

Fixes picked up that affect this agent:
- `importSessionToStore` (used for session forks) no longer skips
transcript rows containing U+2028/U+2029 on Node 24.
- Resuming an SDK session after a compaction no longer loses the recent
messages or the compaction summary in some cases.
- Background subagents in auto mode no longer send their final report
twice or merge it into another turn.
- Queued content-block prompts merged into one turn are now separated by
a line break.
- `ScheduleWakeup` now fires while a background task is running.

Behaviour change: `MAX_THINKING_TOKENS=0` (sent as `thinking: { type:
"disabled" }`) now runs adaptive thinking on models that can't turn
thinking off.

`npm run build`, `npm run check` and `npm run test:run` all pass.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:17:45 +00:00
nikita-ashihmin
1a1f1a0e80 feat: mark session list rows of worktrees Claude Code made (agentWorktree) (#1352)
## What

Marks `session/list` and `_session/list/changes` rows whose session runs
in a linked Git worktree that Claude Code itself created (`claude
--worktree`, the `EnterWorktree` tool), as
`_meta.jetbrains.air.agentWorktree: true`. A new agent capability
`sessionWorktrees`, advertised together with `sessionIndex`, tells the
client that rows carry the field, so a missing field means "not made by
the agent" rather than "unknown".

## Why

With `includeWorktrees`, a client gets sessions from every linked
worktree with nothing to tell them apart. A worktree Claude Code made
for a session belongs to the project the user started it from; a
worktree the user made by hand is a separate checkout. Only the agent
knows which is which.

## How

- The rule reads the transcript's `worktree-state` records: the latest
one wins; the session counts only when `worktreeSession` is non-null and
`enteredExisting` is not `true`. `environment.isWorktree` is not used,
since plain git detection is also true for user-made worktrees.
- The record is parsed in the existing head/tail transcript scan, so no
extra file reads. The row is marked only when its `cwd` is in a linked
worktree (`<common-dir>/worktrees/<name>`), never for the main checkout
or a submodule.
- The row's `cwd` stays the session's own directory. The field is `true`
or absent.

Proposed as `SessionInfo.agentWorktree` plus a `agentWorktrees`
capability marker in the session list extensions RFD:
agentclientprotocol/agent-client-protocol#2323. Docs:
`docs/air-extensions.md`.

## Tests

`src/tests/session-index.test.ts`: marked worktree, `enteredExisting`,
no record (user worktree), a later `null` record, main checkout,
`_session/list/changes` rows, capability negotiation, and scan windows.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:15:52 +04:00
Ben Brandt
0c8de1380f fix: keep a live session that was never prompted when a resume or load changes its settings (#1353)
Found by running the ACP TCK's v2 suite against the adapter
(ACP-ADDDIRS-202). The code is shared, so it also fixes v1's
`session/load` and `session/resume`.

## The bug

A `session/resume` or `session/load` can change a live session's
settings: its `cwd`, additional directories, MCP servers, or the other
settings in its fingerprint. When that happens, `getOrCreateSession`
tears the live query down and starts a new one with `resume: sessionId`.
Claude Code writes the conversation only at the first prompt, so for a
session that was never prompted, the resume failed with `-32002`
"Resource not found". The live session was already torn down by then, so
the client lost a session it had a moment earlier.

## The fix

The sign-out path already handled the same case: it tries `resume`, and
on `resourceNotFound` it starts a fresh query under the same id with
`reuseSessionId`. That logic is now one helper, `recreateLiveSession`,
used by both:
- **`getOrCreateSession`** uses it when the session was live. A session
that was never prompted then starts a fresh query under its own id. It
loses no history, because it had none, and it keeps its mode.
- **A session that isn't loaded here** still resumes directly. If it has
no stored conversation, that still fails with `-32002`, since the
session really is unknown.
- **`recreateSignedOutQuery`** keeps its own handling: if both attempts
fail, it puts the old session object back.

## Checks

- **ACP TCK v2** (`agentclientprotocol/acp-tck` at `fa00ccd`) against
this build: ACP-ADDDIRS-202 now passes, where it failed before.
ACP-ADDDIRS-201 and ACP-RESUME-201 to 205 still pass.
- **Live v1 `session/load`**, Claude Code 2.1.296: create a session,
then load it with a new additional directory before any prompt.
  - On `main`, the load failed with `-32002`.
- With this fix, the load succeeds. A prompt then read a file that
exists only in the new directory, and the transcript was saved under the
same session id.
- **Tests:** three new tests:
- a live session that was never prompted starts a fresh query with its
mode;
  - an unknown session still fails;
- another failure of a live session's resume doesn't start a fresh
query.

Removing the fallback, applying it to every error, or applying it to
sessions that aren't loaded each fails a test. The sign-out tests pass
through the shared helper.
- **Suite:** `npm run check`, build and all 3068 tests pass, with no
golden changes.

**Not covered:** a model chosen with `session/set_config_option` before
the first prompt resets to the default when such a session is recreated,
as it already did after a sign-out. The mode is kept.
2026-10-14 23:16:29 +02:00
Ben Brandt
fadbe1881c fix: let session/delete succeed for a session without a transcript (#1351)
Found by running the ACP TCK's v2 suite against the adapter
(ACP-DELETE-201 and ACP-DELETE-203). The code is shared, so v1 clients
get the fix too.

## The bug

ACP says: "Deleting an already-deleted session, or a session that never
existed, **SHOULD** succeed silently." For a client that is neither AIR
nor a `sessionIndex` client, `session/delete` instead failed with
`-32603` "Internal error" in three cases:

- **A session this connection ran but never prompted.** Claude Code
writes the transcript at the first prompt. So the adapter closed the
live session, then the SDK delete threw "not found in any project
directory".
- **An unknown session.** The SDK delete throws the same "not found"
error.
- **An id that isn't a UUID.** The SDK delete throws "Invalid
sessionId".

## The fix

A new `deleteSessionIfStored` in `src/session-index/service.ts` handles
every other client's delete:
- An id that isn't a session id can't name a transcript, so it returns
`{}` without calling the SDK.
- The SDK's own "not found" means nothing is on disk, so that also
returns `{}`. It's recognised by the existing `isSdkNotFound`.
- Any other SDK failure, such as a permission error, still fails the
request.

The two AIR paths are unchanged, because `docs/air-extensions.md`
documents their errors: `-32002` for an unknown session on a
`sessionIndex` client, and the SDK's error for AIR without
`sessionIndex`. The doc now describes the plain client's behavior.

## Checks

- **ACP TCK v2** (`agentclientprotocol/acp-tck` at `fa00ccd`) against
this build: ACP-DELETE-201 and ACP-DELETE-203 now pass, where both
failed before. ACP-DELETE-202 is still skipped, because `session/list`
doesn't show a session that was never prompted.
- **Tests:** three new tests: a never-prompted live session, an unknown
session, and an id that isn't a UUID. Removing either rule, or
swallowing every error, fails a test. The existing delete tests now use
UUID ids, which is what real sessions have.
- **Suite:** `npm run check`, build and all 3068 tests pass, with no
golden changes.
2026-10-14 23:14:54 +02:00
Ben Brandt
ba3c7be05e feat: keep background subagents running after a parent cancel, for clients that can cancel them (#1348)
Stacked on #1347, which gives each running child the `cancel`
capability. Merge #1347 first; this PR's base is its branch.

## What changes

On a client that gets the subagents RFD form (v1 with
`clientCapabilities.subagents` and not AIR, and v2), `session/cancel` on
the parent now spares background subagents. They keep running, as
acp#2308 prescribes ("a background child that the cancelled work did not
wait on may continue"), and the client can stop each one with its
child's `cancel` from #1347. Foreground subagents that the turn waits on
still stop.

Plain v1 clients and AIR are unchanged: the interrupt still stops their
background subagents, since they have no way to stop one.

## How

- **The declaration.** Claude Code's interrupt kills background
subagents and workflows unless the query declares
`perTaskStopAffordance`. The SDK describes it as "this consumer renders
a per-task stop control", and an RFD client now has that control. The
adapter declares the option for those clients. It comes before
`_meta.claudeCode.options`, so a client can turn it off.
- **Workflows.** The declaration also spares background workflows
(`local_workflow`). A workflow's agents are not subagent tasks and get
no child session, so a client can't stop a workflow unless it is AIR
with `asyncTasks`. When a session declares the option and the client has
no such control, `session/cancel` stops each live background workflow
with `Query.stopTask` after the interrupt. Workflows therefore behave as
they did before.
- **Background commands** survive an interrupt with or without the
declaration (checked live and in the CLI code), so nothing changes for
them.

Where I read Claude Code 2.1.296's CLI: without the declaration, the
interrupt runs a kill of every running background `local_agent` and
`local_workflow` task. With it, the interrupt kills only observer tasks.
Adapter interrupts name no prompt, so this runs on every
`session/cancel`, including one with no turn running. The declaration's
only other effect is on requests with `interrupt_if_running`, which the
adapter never sends.

## Checks

**Live, Claude Code 2.1.296, through the built adapter.** Every update
was checked against the schema, and no run had a violation or left a
tool call open. Survival was checked by whether the background command's
process was alive 2 s after the cancel.

| Client | Background work at the cancel | Result |
|---|---|---|
| v1 RFD | subagent running a command | Prompt `cancelled` 9 ms after
the cancel. The child kept running, completed its command and reported
`idle end_turn`. Claude Code's follow-up cycle then streamed to the
root. |
| v1 RFD | subagent with an open permission request | The request stayed
open through the cancel. The client allowed it 5 s later, and the child
ran the command and ended `idle end_turn`. |
| v1 RFD | workflow | The workflow and its command stopped, with no
follow-up cycle. Same as the plain client. |
| v1 plain | subagent | Unchanged: the interrupt killed the child, and
its command's tool call ended `failed`. |
| v1 plain | workflow | Unchanged: stopped, no follow-up. |
| v2 | subagent | Root `idle cancelled` 11 ms after the cancel. The
child kept running and later reported `idle end_turn`, and the root went
`running`, then `idle end_turn`, for the follow-up. |
| v2 | workflow | Stopped, no follow-up. |

**Direct SDK probes** decided the design:
- With the declaration, an interrupt spares a background workflow.
- Without it, the interrupt kills the workflow.
- A `stopTask` on a workflow, before or after the interrupt, stops it
and its command, and Claude Code starts no follow-up cycle, the same as
when the interrupt killed it.

**Tests:**
- Option tests: the declaration for each client form, a caller's
override, and when a cancel stops workflows.
- Cancel tests: only workflows are stopped, after the interrupt and only
when the flag is set.
- Removing any of the eight rules fails a test.
- `npm run check`, build and all 3020 tests pass, with no golden
changes.

## Not covered

- A paused background workflow (one Claude Code restored on a resume)
would also be stopped at a cancel, where the interrupt left it alone. I
didn't check whether such a workflow reports `task_started`.
- Standard ACP has no control to stop background work that is not a
session, such as a workflow or a background command. Only AIR has one,
through its async tasks extension.
2026-10-14 21:56:01 +02:00
Ben Brandt
aeb69e7ed1 feat: let a client cancel one subagent with session/cancel on its child session (#1347)
A client on the subagents RFD can now stop one subagent without
cancelling the turn. Each running child is announced with `capabilities:
{ cancel: {} }`, and `session/cancel` with the child's session id stops
that child's current work.

## What Claude Code does

From direct SDK probes of `Query.stopTask(taskId)` and runs through the
adapter (Claude Code 2.1.293 and 2.1.296):

| Case | What Claude Code sends |
|---|---|
| Foreground subagent running a command | At once: `task_updated`
`killed` and `task_notification` `stopped` for the subagent, `stopped`
for its command's task, the command's interrupted result, then the Agent
call's interrupted result (`non_execution_kind: "interrupted"`). The
parent's turn goes on and ends normally. |
| Background subagent | The same events and result, then Claude Code's
follow-up cycle about the stop. |
| Subagent with an open permission request | The request is withdrawn
(its `AbortSignal` fires), and both calls get results. |
| Subagent waiting on its own foreground subagent | The grandchild is
stopped too, with its own `killed` and `stopped`, which is the cascade
the RFD requires. |

## The change

- **The grant:** `announceNativeSubagent` grants `cancel` to each live
RFD child. A replayed child gets no capabilities, since it no longer
runs (the RFD's freshness rule for reconnection). A routing-only
ancestor that a restore announces gets none either.
- **The cancel:** `cancel` checks first whether the session id is a
child of one of its sessions (`NativeSubagentRuntime.cancelChild`), and
if so calls `Query.stopTask` with the child's agent id. It sends no
interrupt, so the parent's work goes on. A stop that Claude Code refuses
is logged.
- **The report comes from the SDK's events,** `killed` or `stopped`,
which `finishTask` already turns into `idle` with `cancelled`. Accepting
the stop is no evidence, as the RFD says, and a cancel that races with a
child's end does not rewrite its outcome.
- **Requests:** a request that the stop withdraws reports no `running`
before `idle cancelled`. `awaitingUser` now counts cancels of the child
and of its ancestors, as it counted the root's (#1337). An ancestor's
cancel stops the foreground subagents under it.
- **Unchanged:** AIR keeps its draft and grants nothing, and a cancel of
a root session works as before.

## Evidence

**Live, through the adapter** (Claude Code 2.1.296, Sonnet), every
update checked against the schema, with no violations:

| Run | Child | Parent |
|---|---|---|
| v1 RFD, foreground child running a command | `running`,
`requires_action`, `running`, `idle cancelled` 9 ms after the cancel;
its command `failed` | Agent call `failed`; turn ends `end_turn` |
| v1 RFD, background child while v1 holds the turn | `idle cancelled`;
its command `failed` | the held turn settles with `end_turn` |
| v1 RFD, child with its permission request open | `requires_action`,
then `idle cancelled`, with no `running` between | turn ends `end_turn`
|
| v2, foreground child | `idle cancelled` on the root's stream and its
own | Agent call `cancelled`; root `idle end_turn` |

No tool call was left open in any run.

**Tests:**

- `native-subagents.test.ts`:
- `cancelChild` stops the child's task and reports nothing itself, and
the SDK's `killed` reports `idle cancelled`;
  - no stop for a session that is no child, or for AIR;
- no `running` for a request that the child's own cancel withdraws, or
that its parent's cancel withdraws.
- `acp-agent.test.ts`: `session/cancel` on a child calls `stopTask`, not
`interrupt`, and the turn ends `end_turn`; a refused stop is logged;
nothing happens for an unknown session id or an AIR child.
- `acp-v2.test.ts`: the same over v2, with the child's `idle cancelled`
on both streams.
- **Golden files:** the six v2 subagent golden files gain `capabilities:
{ cancel: {} }` on each announcement, and nothing else. No AIR golden
file changes.
- **Mutation checks:** removing each of these fails at least one test:
  - the grant;
  - the routing to the child;
  - the child's cancel count;
  - the ancestors in the count;
  - the AIR exclusion;
  - the error handling.

  So does reporting `idle cancelled` as soon as the stop is accepted.

`npm run check`, build and the full suite (3014 tests) pass.

## Not changed, and a decision

- **A parent cancel still stops background subagents.** Claude Code does
that because the adapter does not declare the SDK's
`perTaskStopAffordance` option. Declared, an interrupt spares background
agents and background shell commands (checked live: both ran to
completion after the interrupt), as
agentclientprotocol/agent-client-protocol#2308 prefers. Background shell
commands would then outlive a cancel too, and ACP has no control to stop
them, so declaring it is left for a separate decision.
- **Order:** Claude Code sends the stopped child's command result right
after its `killed` event, so the client sees `idle cancelled` a few
milliseconds before that call ends. The RFD has the client mark the
child's unfinished calls cancelled at that point and still accept later
updates.
- **On v2,** the stopped child's command ends `failed`, not `cancelled`:
Claude Code's result for a subagent's command carries no non-execution
kind.
2026-10-14 21:56:01 +02:00
Sergey Shkredov
e8eb9d340f feat: narrow models to what the providers/set gateway lists (#1340)
### Summary

Addresses [IJAI-1783](https://youtrack.jetbrains.com/issue/IJAI-1783).

An organization can restrict the models its JetBrains gateway serves,
but a session routed through `providers/set` offered the whole Claude
Code catalogue, so the picker showed models the gateway then refused.
`providers/set` now lists the gateway's models before it responds, and
the model picker offers only the SDK models the gateway lists.

- `anthropic`: `GET {baseUrl}/v1/models?limit=1000`, paginated by
`has_more`/`last_id`, with the provider headers and `anthropic-version`.
- `bedrock`: `GET {baseUrl}/foundation-models` (`ListFoundationModels`,
`modelSummaries[].modelId`), the listing the JetBrains gateway answers
on its Bedrock route. Only `anthropic.` ids count. The cross-region
prefix `global` when listed, else that of the first listed id, becomes
the session's `ANTHROPIC_BEDROCK_REGION_PREFIX`, so the CLI calls the
models under the prefix the gateway serves; ids of other prefixes, and
SDK rows resolved to another prefix, are dropped.
- Rows match by `resolvedModel` (or `value`) without dates, `-vN:M`
suffixes, `[1m]` hints and provider prefixes; the narrowing runs after
the `availableModels` allowlist. When the gateway does not list the
model a session runs (the dropped default, or a resumed or recreated
session's last model), the session switches to the first listed one with
`setModel`, so the CLI does not keep running an unlisted model.
- The listing runs in the provider-update queue, so `providers/set` and
`providers/disable` apply in the order they arrive; logout also clears a
`providers/set` that is still listing.
- A 4xx answer means the gateway has no listing: `providers/set`
succeeds and nothing is narrowed. Any other failed listing (network
error, 5xx, unexpected body, 10 s timeout) fails `providers/set` with
`internal_error`, and the previous provider stays in effect. An invalid
header or a base URL with credentials fails without repeating the value.
A listing that matches no SDK model leaves the session with no model to
offer: there is no model config option.
- `vertex`, gateways set through `authenticate`, and native routing are
not listed. Setting the same gateway again reuses its listing;
`providers/disable` and logout drop it.
- `docs/model-configuration.md` describes the narrowing.

### Not covered

Background haiku requests, subagents, and a `/model` typed into the
prompt choose their own model and are not checked against the listing.

### Verification

```bash
npm run check
npm run build
npm run test:run -- --no-file-parallelism   # 2964 passed, 5 failed, 30 skipped; all 5 in session-index.test.ts, flaky on main too
```

Tests stub `fetch` (`src/tests/setup-no-network.ts` answers an empty
listing by default) and cover the listing requests and pagination, the
Bedrock region prefix and its env, region-aware matching, the dropped
default, the failing `providers/set`, and the empty picker. The
JetBrains Bedrock gateway's `/foundation-models` answers
`global.anthropic.claude-{haiku,opus,sonnet}-5-5` (seen through Junie,
which lists the same route); this adapter has not yet been run end to
end against it.

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:03:27 +02:00
Ben Brandt
d528647162 fix: report no running for the session's own request that a cancel withdraws, on ACP v2 (#1346)
## Problem

On ACP v2 the adapter reports a session's foreground work as
`state_update`s: `running`, then `requires_action` while a permission
request or question that blocks the work is open, then `running` again,
and finally `idle` with a stop reason. A request that ends because
`session/cancel` or `session/close` stopped the work is not an answer,
but it still reported `running`. #1337 fixed that for a subagent's child
session and listed the session's own request under "Not covered". That
request did not change, and since #1338 neither did its AskUserQuestion,
which now ends at the abort too.

On `main`, with Claude Code 2.1.296, v2, a Bash permission request that
the client holds open, and then a cancel or a close:

| What stops the work | States the session reported |
| --- | --- |
| `session/cancel` | `running`, `requires_action`, **`running`**, `idle
cancelled` |
| `session/close` | `running`, `requires_action`, **`running`**, `idle
cancelled`, all before the close response |

Claude Code's interrupt withdraws the request, so the SDK aborts the
signal of the `canUseTool` call, the request's `finally` calls
`syncAwaitingUser`, and with no blocking request left it reports
`running`. The result that ends the work comes 2 to 9 ms later in these
runs, so the client shows work that is being stopped as working.

## Cause

`syncAwaitingUser` reported `running` whenever the last blocking request
ended, whatever ended it. A request ends when the user answers it or
when Claude Code withdraws it, and a withdrawal alone does not say
whether the work was stopped. A `PermissionRequest` hook that answers
also makes Claude Code withdraw the request, and the work goes on (9 s
more in a live run), as #1337 found. So "a withdrawn request reports no
`running`" is wrong. The rule that fits is #1337's: no `running` when
the request was withdrawn after a cancel that came while it was open.

## Change

- `cancelTurns` (called for `session/cancel`, and by the teardown of
`session/close`) marks each request that is open, in
`Session.openUserInputs`.
- `withPendingUserInput` takes the request's `signal`. When the request
ends with its signal aborted and the mark set, `syncAwaitingUser` stops
waiting but reports no `running`. The four callers pass their signal:
permission requests, AskUserQuestion, MCP elicitation, and the refusal
fallback prompt. (#1337 counts the parent's cancels and compares the
count when the request ends. Here each open request is already an entry
of `openUserInputs`, so the cancel marks it. Same rule.)
- Everything else is as before:
  - an answered request reports `running`, also after the cancel;
  - a withdrawal without a cancel reports `running`;
- with two requests open, only the last one to end reports, by how that
one ended;
- a request that opens after the cancel is judged on its own, so a hook
that withdraws it still reports `running`.
- A foreground subagent's request blocks the turn, so it behaves like
the turn's own. A background subagent's request does not block it
(`blocksForeground`), so the root's states do not change for it. #1337's
child states are unchanged.
- `ForegroundEvents.running` and `v2Foreground` say when `running` is
not reported.

v1 reports no session state, so nothing changes there. AIR is unchanged:
the scenario golden files do not change.

## Evidence

Live runs against Claude Code 2.1.296 (Sonnet), driving `dist/index.js`
over stdio as a v2 client (`CLAUDE_AGENT_ACP_EXPERIMENTAL_V2=1`) that
holds the request and ignores `$/cancel_request`, with the session's cwd
in default permission mode. Every update and request validated against
the SDK's v2 JSON schema: 0 violations in all runs. The root's states:

| Run | `main` | This PR |
| --- | --- | --- |
| `session/cancel`, Bash permission request | `running`,
`requires_action`, `running`, `idle cancelled` | `running`,
`requires_action`, `idle cancelled` |
| `session/cancel`, AskUserQuestion | `running`, `requires_action`,
`running`, `idle cancelled` | `running`, `requires_action`, `idle
cancelled` |
| `session/close` | `running`, `requires_action`, `running`, `idle
cancelled` | `running`, `requires_action`, `idle cancelled` |
| `session/cancel`, request of a foreground subagent (#1337's run) |
root as above; child `running`, `requires_action`, `idle cancelled` |
root `running`, `requires_action`, `idle cancelled`; child the same |
| `PermissionRequest` hook allows after 3 s, no cancel | `running`,
`requires_action`, `running`, `idle end_turn` | the same |
| the client answers after 2 s | `running`, `requires_action`,
`running`, `idle end_turn` | the same |
| `kill -9` of Claude Code, no cancel | `running`, `requires_action`,
`running`, `idle error` | the same (see below) |

The hook run is why a withdrawal alone cannot mean "stopped": Claude
Code withdrew the request 3 s in, and the command then ran 9 s more. A
v1 run of the cancel (no state updates there) is identical on both
builds.

Tests, in `acp-v2.test.ts`, through the v2 client app, which validates
every update:

- For each of the four kinds of request (permission request,
AskUserQuestion, an MCP server's elicitation, the refusal fallback
prompt): a `session/cancel` withdraws it, a `session/close` withdraws it
(the harness gets a `close` hook, as the SDK's cleanup of a closed query
aborts open requests), and Claude Code withdraws it without a cancel.
- The user answers after the cancel; a request that opens after the
cancel and is then withdrawn; two requests open together (both withdrawn
by the cancel, and one withdrawn and the other answered, in each order);
a request of a cycle that Claude Code started.
- Subagents: a foreground subagent's request also holds the root (an
assertion added to #1337's test); a background subagent's request
changes nothing of the root's states, alone and next to the turn's own
request.

13 of them fail on `main` (12 new tests and the extended one); the other
8 new tests pass there and guard what must not change.

Removal checks, each run against `acp-v2.test.ts`:

| Removed or changed | Tests that fail |
| --- | --- |
| no rule: the end always reports `running` | 13 |
| the cancel marks no request | 13 |
| a mark is enough, the signal is not required | 2 (answered after the
cancel) |
| the simple rule "any withdrawal means stopped" | 5 (the hook cases,
and the request after the cancel) |
| `session.cancelled` in place of the mark of a cancel that came while
the request was open | 1 (the request after the cancel) |
| sticky: after one request was withdrawn by a cancel, no later request
reports `running` | 1 (answers the one the cancel did not withdraw) |
| permission requests do not pass their signal | 6 |
| AskUserQuestion does not pass its signal | 3 |
| MCP elicitation does not pass its signal | 2 |
| the refusal fallback prompt does not pass its signal | 2 |

`npm run check`, `npm run build` and `npm run test:run` pass (2999 tests
passed, 30 skipped; the scenario golden files do not change).

## Not covered

- **A stream end without a cancel.** If Claude Code exits by itself, the
SDK's cleanup aborts every request before the consumer reads the end of
the stream, and nothing at that moment tells it from a hook's
withdrawal. Live (`kill -9` of the CLI, table above): `running` 2 ms
before the `idle`, and the session is dead right after. Left as it is,
for the reasons #1337 gives for children.
- The rule assumes, like #1337's, that the work reports its end right
after such a withdrawal. A turn always does (the force-cancel backstop
ends a wedged one); a cycle with no active turn has no such backstop.
- Open PRs #1344 and #1345 also change `src/acp-agent.ts`; this branch
merges cleanly with each (checked with `git merge-tree`).
2026-10-14 13:22:12 +02:00
Ben Brandt
594aa8c955 fix: close the tool calls that a cycle Claude Code started abandoned, at its end (#1344)
A model cycle that Claude Code starts on its own now closes, at its end,
the tool calls that Claude abandoned before they ran, as a turn's end
already does.

## The bug

Claude can start a tool use and never finish the message that holds it,
for example when the stream is cut off. The tool never runs, and no
result comes. At a turn's end, `settleActive` closes such a call
(`closeUnfinished`: "Claude stopped this tool call before it ran.",
`failed` on v1, `cancelled` on v2). A cycle that Claude Code started on
its own, such as its follow-up after a background command finished, has
no turn. So nothing closed its abandoned calls, and the client kept them
open.

## The fix

`closeCancelledCycleToolCalls` becomes `closeCycleToolCalls` and runs at
every end of a cycle: its result, or an idle that no result precedes.

- **After a cancel:** unchanged (#1339, #1342).
- **At any other end:** every call that ran already has its result. What
is open is a call of the main conversation that was never dispatched,
which is closed with the same report a turn's end sends. A subagent's
calls are not the cycle's: a background subagent runs on.

On v2 the report comes before the cycle's `idle`.

## Evidence

**Tests:**

- v1, a cycle that runs a command, starts a background subagent that is
streaming a tool use as the cycle ends, and abandons a tool use: only
the abandoned call is closed, once. The command keeps its one result,
and the subagent's call stays open.
- v1, a cycle that ends at an idle with no result (the stream dropped
mid-cycle): its abandoned call is closed there.
- v2: the abandoned call goes `pending`, then `cancelled`, before the
cycle's `idle end_turn`.
- **Mutation checks:** each of these fails at least one test:
  - no closing at a normal end;
  - closing a subagent's calls;
  - closing dispatched calls (an existing test catches it);
  - closing only at the result, not at the idle.

`npm run check`, build and the full suite (2982 tests) pass, with no
golden changes.

**Not checked live:** Claude abandons a tool use only when something
cuts its message off, such as a steer or a dropped stream, which a live
run cannot set up reliably.
2026-10-14 13:21:04 +02:00
Ben Brandt
bcc024d2c9 fix: deliver the output of a background subagent that a cancel did not stop (#1345)
After a cancel, the output of a background subagent that the cancel did
not stop now reaches the client: its tool results, its new tool calls,
and its permission requests.

## The bug

The adapter has one rule for work that a cancel does not stop, as
agentclientprotocol/agent-client-protocol#2308 asks: a background
subagent that still runs (`runsInBackground`). The closing at the cancel
leaves that subagent's tool calls open for their results
(`closedAtCancel`). But the window after the cancel dropped the same
subagent's output, results included:

- **A subagent that completes after the cancel** kept its calls open.
Its commands' results came after the cancel was reported and were
dropped, and since it was not stopped, nothing closed them.
- **A subagent that runs on** after the cancel showed nothing more: new
tool calls were dropped, and its permission requests were denied as the
stopped work's (#1334).

Claude Code currently stops background subagents at an interrupt, as
live runs show, so today this applies only to the output that comes
before the SDK reports a subagent's end. It decides the rest once Claude
Code lets them run.

## The fix

`isRunningBackgroundSubagentOutput` uses the closing's predicate for the
three places the window applies:

- the drop of streamed output;
- the record that `canUseTool` and the v2 preview hook read (#1334);
- the guard of the user/assistant messages.

A subagent's output after the SDK reported its end is still dropped, and
#1336 closes its calls.

## Evidence

**Tests** (`src/tests/incomplete-tools.test.ts`):

- **A command's own result,** for a subagent that completes after the
cancel, reaches the client, for a held turn and with no turn. This
replaces "leaves the command of a subagent that completed after the
cancel as it is", whose fixture sent no result at all.
- **New work after the cancel was reported:** a command's streamed
start, complete message and result reach the client.
- **Permission requests after the cancel:** they are allowed, for a tool
use that streamed first and for one that only its complete message
holds.
- **Unchanged:** a stopped subagent's late result is still dropped, and
its call closed.
- **Mutation checks:** removing each of the three uses, or widening the
predicate to every subagent, fails at least one test.

**Live, Claude Code 2.1.296,** a v1 cancel of a turn held for a
background subagent that runs a command, on a plain client and an RFD
client: Claude Code stopped the subagent, and its command was closed
after the cancelled response, as on `main`. No schema violations. A
subagent that survives a cancel cannot be set up live today.

`npm run check`, build and the full suite (2983 tests) pass, with no
golden changes.

## Not covered

- **A foreground subagent nested in a background one:** its own Agent
call is not backgrounded, so the closing closes its calls at the cancel,
and the window drops its output, both as before. The two stay
consistent.
2026-10-14 13:20:36 +02:00
Ben Brandt
b4788d61ea fix: end the tool calls of a cycle that Claude Code started and a cancel stopped, on v1 (#1342)
On v1, when a cancel stops a model cycle that Claude Code started on its
own, the cycle's open tool calls now end. Before, they stayed `pending`
for good.

## The bug

Example, from a live run (Claude Code 2.1.296, a v1 client):

1. A prompt starts a background command, and its turn ends.
2. The command finishes, and Claude Code starts a cycle on its own: it
launches a foreground subagent, which runs a command.
3. The client sends `session/cancel` while no prompt runs.

Claude Code stops both and sends the Agent call's interrupted result
(`user-rejected`). The client never got it, and the Agent call stayed
`pending`. Before #1336, the subagent's command stayed `pending` too.

**Why:** the window after a cancel drops output once the client has been
told that the stopped work ended (#1313). On v1 that report is the
prompt response, read as "no active turn". A cycle never has a turn, so
from the cancel on, everything it sent counted as late output, including
Claude Code's results for its own tool calls. v2 tracks a cycle as
foreground work and closes its calls before `idle cancelled` (#1339). v1
tracked nothing.

## The fix

- **The agent tracks foreground work on every connection.** Only v2
reports it, through the `foreground` option; v1 gets events that do
nothing (`UNREPORTED_FOREGROUND`). The rules for when a cycle starts and
ends are the ones v2 already uses.
- **On v1, a cycle's end is its result or its idle**
(`workEndReported`). Until then, Claude Code's interrupted results reach
the client. At that end, `closeCancelledCycleToolCalls` closes the calls
that got none, as on v2. After it, the window drops what comes, as
before. A v1 turn's rule is unchanged: no active turn.
- **A held turn's follow-up shares the held turn's closing.** v1 holds a
turn for its background subagents, and the held turn's follow-up is a
cycle too, now on v1 as well. When a second cancel settled the held turn
in `cancelTurns` while the cycle's closing was in flight, the two ran
separate passes. The response then came before the cycle's first report,
and on v2 a call could be closed twice. `closeCancelledCycleToolCalls`
now uses the held turn's closing (`Turn.closingCancelledToolCalls`),
which every settle of the turn waits for. The existing test "closes each
tool call once, before the response, when a cancel settles a held turn
that the consumer is closing" caught this.

## Evidence

**Live, Claude Code 2.1.296, the scenario above**, every update checked
against the schema (no violations):

| Client | Agent call | Subagent's command |
|---|---|---|
| v1, before | `pending` | `failed` (closed by #1336) |
| v1, this PR | `failed`, with Claude Code's own `user-rejected` result
| `failed`, closed at the cycle's end |
| v1 RFD client, this PR | the same | the same, in the child's session |
| v2, this PR | `cancelled`, before `idle cancelled` | `cancelled`,
before `idle cancelled` (unchanged) |

A v1 cancel with a queued prompt, followed by the next prompt, still
left no tool call open, and the next prompt's Write asked for its
permission and ran.

**Tests** (`src/tests/incomplete-tools.test.ts`):

- **#1339's test for v1 now asserts the fix:** a cycle's command gets
Claude Code's interrupted result.
- **New:** the live scenario, where the Agent call gets Claude Code's
result and the subagent's command is closed; and a cycle that ends at an
idle with no result, where its call is closed at the idle and Claude
Code's later result is dropped.
- **#1336's "no turn" fixture** launched the background subagents in a
cycle it never ended before the cancel, so v1 now delivered Claude
Code's real result there. The fixture now ends that cycle, which is the
"cancel while nothing runs" case the tests mean.
- **Mutation checks:** each of these fails at least one test:
  - the v1 rule without the cycle;
  - no cycles on v1;
  - no foreground tracking on v1;
  - a separate closing for a held turn's follow-up.

`npm run check`, build and the full suite (2979 tests) pass, with no
golden changes.

## Not covered

- A cycle that ends normally with a `tool_use` that Claude abandoned
keeps that call open.
- A subagent that completes after a cancel keeps its calls open.
2026-10-14 12:19:29 +02:00
Ben Brandt
c1e83b46b7 fix: stop waiting for a question the client leaves open after Claude Code cancels it (#1338)
## Problem

A permission request is abandoned when Claude Code withdraws it: its
`signal` is raced against the client's answer
(`requestPermissionFromClient`), because the SDK only sends
`$/cancel_request` and keeps waiting for the client's reply, which a
client that ignores the cancel never sends. An elicitation is not raced.
A question that the client leaves open after Claude Code cancels it
never settles, and its request stays open on our side:

- **The session** keeps the question in `openUserInputs`
(`withPendingUserInput`) for good. Steering stays non-interrupting
(`priority: later`) for the rest of the session, and on v2 the next turn
starts as `requires_action` though nothing is open.
- **A child** that asked keeps its `openRequests` up. When it is resumed
later, `resumeChild` reports `requires_action` instead of `running`.

Live, on `main` (Claude Code 2.1.296, v2): the main conversation asks
with AskUserQuestion, the client holds the elicitation and ignores
`$/cancel_request`, the user cancels, then sends a second prompt, "Reply
with just the word hi.":

| | States of the second turn |
| --- | --- |
| `main` | `running`, **`requires_action`**, `idle end_turn` (the answer
"hi" streams while it waits) |
| This PR | `running`, `idle end_turn` |

## Change

One helper, `elicitFromClient`, sends the elicitation and races it
against the callback's `signal`, as `requestPermissionFromClient` does.
The three places that send an elicitation for a callback of Claude Code
use it:

- AskUserQuestion (`handleAskUserQuestion`), the one that a child can
send;
- MCP elicitations (`onElicitation`, `handleMcpElicitation`);
- the refusal-fallback dialog (`onUserDialog`, `handleUserDialog`).

They had the same defect, each tracks its request with
`withPendingUserInput`, and their handlers already read an abort as a
cancel (`{ action: "cancel" }`, `{ behavior: "cancelled" }`, "Tool use
aborted"), so only the wait changes.

Nothing else in the flow needs the answer to arrive. After the
elicitation, a question's tool call ends with Claude Code's own
`tool_result`. A url-mode elicitation completes with the SDK's
`elicitation_complete`, not with the answer. A client that does reply to
the cancel still settles it first, to the same outcome as before.

AIR is unchanged, and the golden files do not change.

## Tests

All of them fail without the helper (checked by removing the race, and
then by reverting each of the three call sites alone):

- `acp-agent.test.ts`, "does not wait for … when its client ignores
cancellation": each of the three paths ends as aborted and leaves
nothing open.
- `acp-agent.test.ts`, "uses priority:'now' again once … is withdrawn":
the existing table of every request that waits for the user (permission,
AskUserQuestion, MCP elicitation, refusal dialog, and a child's
permission request and question), with a client that never settles. The
permission rows are controls that already passed. I moved the table into
a constant so both tests use it.
- `acp-agent.test.ts`: a child's question that Claude Code withdraws,
and a SendMessage resume after its end, reports `running`.
- `acp-v2.test.ts`: the live scenario above: the next turn does not
report `requires_action`.

Removal checks: no race in the helper (9 tests fail), the
AskUserQuestion site reverted (5), the MCP site (2), the dialog site
(2).

`npm run check`, `npm run build` and `npm run test:run` pass (2935
tests).

## Not covered

- **A child's question live.** Claude Code 2.1.296 does not give
subagents AskUserQuestion: a general-purpose subagent, and a custom
agent with `tools: AskUserQuestion`, both reported that they could not
ask. So the child's path is covered by the unit test only. The root's
question runs the same `handleAskUserQuestion`, which the live run above
covers.
- **The root session at a cancel.** Now that the question settles at the
abort, the root reports `requires_action`, `running`, `idle cancelled`
for it, as it already does for a permission request (live, v2). That
flicker comes from `syncAwaitingUser`, and is not changed here.
- A signal that already aborted when the elicitation is sent still sends
it, and cancels it at once. A permission request checks first. Nothing
depends on it.
- The URL elicitation of an MCP server's sign-in
(`authenticateMcpServer`) keeps its own race against the authentication,
and is not a tracked request.

Related, and independent: #1337 reports no `running` for a child's
request that a cancel withdraws. The two PRs touch the same call site
and merge cleanly in either order (checked with `git merge-tree`).
2026-10-14 11:48:41 +02:00
Ben Brandt
c7425e4e04 fix: report no running for a subagent's request that a cancel withdraws, for subagents RFD clients (#1337)
## Problem

For a subagents RFD client, a subagent's child session reports
`requires_action` while one of its permission or question requests is
open, and `running` again once its last open request ends (#1322,
`NativeSubagentRuntime.awaitingUser`). A request that ends because the
work was **stopped** is not an answer, but it still reported `running`.

On `main`, with Claude Code 2.1.296, a foreground child whose Bash
permission request is open, and a cancel on the parent:

| What stops the work | States the child reported |
| --- | --- |
| `session/cancel` (v1, and v2 on the child's own stream) | `running`,
`requires_action`, **`running`**, `idle cancelled` |
| `session/close` | `running`, `requires_action`, **`running`**, and
then nothing: a closed session reports no `idle` |

Claude Code's interrupt withdraws the request, so the SDK aborts the
signal of the `canUseTool` call, the adapter sends `$/cancel_request`,
and the request's `finally` reports `running`. The SDK's own report of
the child's end (`task_updated killed`) comes a few ms later (3 to 4 ms
in these runs), so the client shows a child that is being stopped as
working. After a `session/close` nothing corrects it.

## Cause

`awaitingUser` reported `running` whenever the last open request ended,
whatever ended it. A request ends in two ways, the user answers it or
Claude Code withdraws it, and a withdrawal alone does not say whether
the work was stopped. Claude Code also withdraws a request when
something else answers it, and then the child runs on. I ran one such
case, a `PermissionRequest` hook that allows the command after 3 s: the
child worked for 9 more seconds after the withdrawal, and `running` was
right. (The CLI also re-checks a pending ask when its permission context
changes, and withdraws it if that now allows the call. I did not manage
to trigger that: `session/set_mode` to `bypassPermissions` left the
request open in my run.) So "a withdrawn request reports no `running`"
is wrong: that child would sit on `requires_action` for the rest of its
run. Waiting for the child's next update instead does not help either:
in that run the child's first update after the withdrawal came 8 s
later, with the tool's result.

## Change

A request reports no `running` when it was **withdrawn after a cancel
that came while it was open**. Then Claude Code's interrupt withdrew it
as it stopped the child, and the SDK's events report the child `idle`.

- `NativeSubagentRuntime.parentCancelled` (called for `session/cancel`,
and by the teardown of `session/close`) counts the cancels in the RFD
form. It still reports nothing, so #2308 is unchanged: a child that
keeps running keeps its requests and its state.
- `awaitingUser` takes the request's `signal`, which the agent passes
for permission requests and AskUserQuestion (`withChildAwaitingUser`). A
request records the count when it opens. If its signal aborted and the
count changed, its end reports no `running`.
- Everything else is as before: a request that the user answers, also
after the cancel, reports `running`; a withdrawal without a cancel
reports `running`; two overlapping requests report once, when the last
one ends, by how that one ended.
- README: one clause in the work-state bullet of "Subagent sessions".

AIR is unchanged: `awaitingUser` only runs in the RFD form, and the AIR
golden files do not change.

## Evidence

Live runs against Claude Code 2.1.296 (Sonnet), driving `dist/index.js`
over stdio with a client that holds the child's permission request,
validating every update and request against the SDK's JSON schema (0
violations in all runs):

| Run | `main` | This PR |
| --- | --- | --- |
| v1, `session/cancel` | `running`, `requires_action`, `running`, `idle
cancelled` | `running`, `requires_action`, `idle cancelled` |
| v2, `session/cancel` (child's stream) | `running`, `requires_action`,
`running`, `idle cancelled` | `running`, `requires_action`, `idle
cancelled` |
| v1, `session/close` | `running`, `requires_action`, `running` |
`running`, `requires_action` |
| v1, hook allows after 3 s, no cancel | `running`, `requires_action`,
`running`, `idle end_turn` | the same |

Tests (all fail without the rule, as checked by removing it):

- `native-subagents.test.ts`: the cancel withdrawal (and the child's
`idle cancelled` after it); a withdrawal without a cancel (the hook
case, which a simpler "abort means stopped" rule fails); a request that
outlives a cancel and is answered later; a cancel that came before the
request opened; two overlapping requests, in each order.
- `acp-agent.test.ts`: through `canUseTool`, with `agent.cancel` and
`agent.closeSession` aborting the request as Claude Code does, and the
hook case.
- `acp-v2.test.ts`: the child's own `state_update` stream and the
parent's copy.

Removal checks, each run against the three test files: no rule (6 tests
fail), the cancel not counted (6), the agent not passing the signal (3:
agent cancel and close, v2), and the simple "any withdrawal" rule (3:
the hook cases).

`npm run check`, `npm run build` and `npm run test:run` pass (2935
tests; the scenario golden files do not change).

## Not covered

- **A stream end without a cancel.** If Claude Code exits by itself, the
SDK's cleanup aborts every request before the consumer reads the end of
the stream, and nothing at that moment tells it from a hook's
withdrawal. Live (`kill -9` of the CLI): `running`, then `idle`, 1 ms
apart. The session is dead right after. A rule for it would be a timing
one: report `running` for a withdrawal with no cancel one event-loop
turn later, so that `finishAll` goes first. I tried it (`await new
Promise((resolve) => setImmediate(resolve))` before the check in
`awaitingUser`'s `finally`): the `kill -9` run then reports `running`,
`requires_action`, `idle`, and the hook run still reports `running` 1 ms
after the withdrawal. I did not include it. It only works when the end
of the stream reaches `finishAll` in the same turn (a worker shutdown
publishes its failure notice first, for one), and it hides an ordering
assumption about the consumer. Your call.
- **Claude Code stopping a child on its own** (the model's TaskStop)
while its request is open: the same shape, no cancel.
- **The root session on v2** has the same flicker for its own request:
`requires_action`, `running`, `idle cancelled` (live, v2). It comes from
`syncAwaitingUser`, not from this runtime, and is not changed here.
- A background child that outlives a cancel and whose still-open request
a hook then withdraws would report no `running`. Claude Code stops
background children at an interrupt today, so it does not arise.

Related, and independent: a child's AskUserQuestion is not abandoned on
an abort when its client ignores `$/cancel_request` (#1338). The two PRs
touch the same call site and merge cleanly in either order (checked with
`git merge-tree`).
2026-10-14 11:47:28 +02:00
Ben Brandt
1fd217f3e7 fix: close the tool calls of a cycle that Claude Code started and a cancel stopped (#1339)
## Problem

On ACP v2, a model cycle that Claude Code starts on its own, such as its
follow-up after a background command finished, is foreground work:
`state_update` `running`, then `idle`. When a cancel stops such a cycle,
the tool calls that it left open can stay `pending` after `idle`
`cancelled`.

Claude Code answers each tool of the main conversation with an
interrupted result, and the adapter delivers it before the cycle's
`idle`. It sends nothing for what a foreground subagent of the cycle
ran, nor for a tool call that Claude abandoned before it ran. No turn
settles at the cycle's end, so `closeCancelledToolCalls` (#1327, #1221)
never runs for it.

Example, live (Claude Code 2.1.296, Sonnet): a background `sleep 6` ends
its turn. When it finishes, Claude Code's follow-up runs a foreground
subagent, which runs a 60 s command, and the client cancels while it
runs:

```
root  state_update running                 <- Claude Code's own cycle
child tool_call gKT4K4 (the subagent's command) pending
      ... session/cancel ...
root  tool_call_update PLh9rB (Agent call) cancelled     <- Claude Code's result
child state_update idle cancelled
root  state_update idle cancelled
child gKT4K4: still pending, for good
```

## Change

- `closeCancelledCycleToolCalls` ends the open tool calls with the
cancelled report before the cycle's end is reported, at the cycle's
result and at the idle that ends it when no result comes. The calls are
the ones the turn's closing takes: every open call except those of a
background subagent that still runs.
- It applies when a cancel is in effect (`session.cancelled`) and the
cycle is the foreground work (`runsAutonomousCycle`). A turn that took
the cycle over closes its own calls when it settles.
- The loop of `closeCancelledToolCalls` is now `closeOpenToolCalls`, and
its filter is named `closedAtCancel`. These two hunks are the same as in
#1336, so that the PRs merge in any order.
- The follow-up cycle of a turn that v1 holds for its background
subagents is a cycle on v2, and its closing now comes before its `idle`.
It came at the held turn's settle, after it.

## Evidence

**Tests** (6 new):

- `acp-v2.test.ts`: the command of the cycle's foreground subagent ends
`cancelled` in the child's session, after the child's idle and before
the cycle's, once; a tool call that Claude abandoned in the cycle ends
before `idle` `cancelled`; so does one at the idle that ends a cycle
without a result; the follow-up cycle of a held turn closes before
`idle` `cancelled`, and a background subagent that keeps running keeps
its command open; a cycle that the SDK aborted without a cancel is left
to Claude Code's result, which still reaches the client.
- `incomplete-tools.test.ts`: on v1, which reports no cycle, a cancelled
cycle's tool call is not closed (see below).

**Removal checks.** Each of these fails at least one test: no closing at
the result (3), none at the idle (1), closing a cycle that no cancel
stopped (1), without the cycle being the foreground work (2), closing a
running background subagent's calls (1), closing after the cycle's end
is reported (3), the turn's closing taking those calls too (5).

**Live**, every update validated against the SDK's v2 JSON schema (0
violations):

| cancel while Claude Code's cycle runs | `main` | this PR |
| --- | --- | --- |
| a foreground subagent's command | `pending` in the child's session,
after the root's `idle cancelled` | `cancelled` in the child's session,
after the child's idle and before the root's |
| a command of the cycle itself | Claude Code's result, `cancelled`,
before `idle` | the same, unchanged |

`npm run check`, `npm run build` and `npm run test:run` (2930 tests)
pass. No golden file changes. With #1335 and #1336 merged into it, in
any of the six orders, the branches merge without conflicts and all 2948
tests pass.

## Not covered, and a decision for the maintainer

- **v1.** Nothing tracks a cycle on v1, and with no turn active every
result after the cancel is dropped, Claude Code's for the cycle's own
commands too. In the same live run on v1 the Agent call and the
subagent's command both stay `pending`. Closing at the cycle's cancelled
result there would end both, with a made-up status for the Agent call
too. I left it: it changes what v1 clients see for a cancel while idle,
and one existing test (a held turn that the consumer closes while a
cancel settles it inline) fails once the cycle check is removed, so it
is not only a gate. It also touches the post-cancel window of #1061,
which another change owns.
- A cycle that ends without a cancel keeps an abandoned `tool_use` open.
`settleActive` closes that for a turn, and no turn owns a cycle.
- A session that ends or closes with a cycle running ends the cycle as
cancelled without closing its calls.
2026-10-14 11:46:20 +02:00
Ben Brandt
9a1b90fb9d fix: close the tool calls of a background subagent that the SDK reports stopped after a cancel (#1336)
## Problem

After `session/cancel`, the tool call of a **background** subagent's
command stays `pending` for good when no turn is left to settle.

Example, live (Claude Code 2.1.296, Sonnet): a background subagent runs
`python3 -c "import time; time.sleep(60)"`, and the client cancels while
it runs. Either the session is idle, or v1 holds the prompt open for the
subagent and the cancel settles it at once. Claude Code's interrupt
stops the subagent: the SDK reports `task_updated` `killed` and
`task_notification` `stopped`, and the RFD form reports the child `idle`
`cancelled`. The command's call is never closed, in the root session or
in the child's.

## Cause

#1327 closes a cancelled turn's open tool calls when the turn settles
(`closeCancelledToolCalls`), and leaves a background subagent's calls
open, because the interrupt may not stop it. Here the turn is already
settled (the cancel settled the held turn before the interrupt) or there
is none, and the stop comes after that. Nothing closes the calls then.

Claude Code does send the command's interrupted result. It comes after
the stop reports, so it is output after the work's end, and the
`session.cancelled` check of the user/assistant case drops it, as it
drops everything after that. The raw stream, in the order Claude Code
sends it:

```
task_updated      agent patch killed
task_notification agent stopped
task_notification the command's task stopped
user (ptu=agent)  tool_result for the command, is_error     <- dropped
```

## Change

- **A subagent's stop closes its open tool calls**
(`closeStoppedSubagentToolCalls`), at `task_notification` `stopped` and
at `task_updated` `killed`, after `finishTask` has reported the child
idle. It closes the calls whose recorded parent is that subagent's Agent
call (`Session.toolCallParents`) with the cancelled report that #1327
sends, naming the subagent so that every client form routes it like the
call's other updates.
- **Only where nothing else will end the calls**: a cancel is in effect
(`session.cancelled`), and the client was told that the work ended
(`workEndReported`: the prompt response on v1, `idle` on v2). While the
work runs, a stopped subagent's calls get Claude Code's own result or
`closeCancelledToolCalls` at the work's end, as before.
- The loop of `closeCancelledToolCalls` is now `closeOpenToolCalls`,
which takes the filter for the calls: the turn's closing, named
`closedAtCancel`, skips a running background subagent's calls, and this
closing takes one subagent's. These hunks are the same as in #1339,
which closes the calls of a cycle that Claude Code started, so that the
PRs merge in any order.
- README: one sentence on when such a call ends.

Nothing is closed twice: a closing forgets the call before it reports
it, so a later closing, and the result that Claude Code sends after the
stop, find nothing. The cancel settles a held turn and closes its calls
before it interrupts Claude Code, so the stop reports always come after
that closing.

## Per client form

- **v1 without subagents:** the call goes to the root session, after the
response when the cancel settled a held turn.
- **RFD:** the call ends in the child's session right after the child's
`idle` `cancelled`, as #1327 does at a turn's settle.
- **AIR:** the cancel already ended every child, so the runtime drops
the report (`ignoring late update for terminal subagent`). The call's
state is released, which it was not before.
- **v2:** as RFD. The root reports nothing, since it had no work.

## Evidence

**Tests** (10 new):

- `incomplete-tools.test.ts`: for a held turn and for no turn, the
command ends once with the cancelled report, after the cancel's
response, and the other subagent's command stays open; each of the two
reports ends it alone; a subagent that completed after the cancel is
left alone; one that stopped without a cancel, and one that stopped
while the work still runs, get Claude Code's result, not a made-up
report.
- `acp-agent.test.ts`: the RFD form (the child's session, after its
idle, after the response) and AIR (nothing reaches the ended child).
- `acp-v2.test.ts`: the child's command ends `cancelled` in its session
after its `idle cancelled`, and the root reports nothing more.

**Removal checks.** Each of these fails at least one test: no closing at
the notification (2), none at the patch (1), closing without a cancel
(1), before the work's end is reported (2), at any status of the
notification (1), of the patch (1), every subagent's calls (4), calls of
another parent (7), the turn's closing no longer skipping a running
background subagent (7), the report without the subagent's name (7), and
the closing before the child reports idle (2).

**Live**, 60 s command in a background subagent, cancel 4 s after its
permission request was answered, every update validated against the
SDK's JSON schema (0 violations):

| client | cancel while | `main` | this PR |
| --- | --- | --- | --- |
| v1, no capabilities | the held turn waits for the subagent |
`pending`, response `cancelled` | `failed` 4 ms after the response |
| v1, no capabilities | no turn runs | `pending` | `failed` after the
SDK's first stop report |
| v1, RFD | no turn runs | `pending` in the child's session | `failed`
in the child's session, after its `idle` `cancelled` |
| v1, RFD | the held turn | not run on `main` | `failed` in the child's
session, after the response |
| v1, AIR | the held turn | not closed | report dropped by the runtime,
state released |
| v2 | root idle | `pending` in the child's session | `cancelled` in the
child's session, after its `idle cancelled` |

`npm run check`, `npm run build` and `npm run test:run` (2934 tests)
pass. No golden file changes. With #1335 and #1339 merged into it, in
any of the six orders, the branch merges without conflicts and all 2948
tests pass.

## For the maintainer

- **Closing, not delivering.** Claude Code 2.1.296 sends the result, so
delivering it after the work's end would also end the call, with Claude
Code's text. I closed instead: 2.1.293 sent none for a subagent's call
(#1327), the result comes after the response in the window #1313 closes,
and closing works for both. The cost is the made-up text ("The turn was
cancelled before this tool call returned a result.") in place of Claude
Code's.

## Not covered

- A background subagent that the interrupt does not stop keeps running,
and its calls stay open. Its output after a cancel is dropped until the
next prompt, as before.
- A subagent that completes after a cancel: Claude Code's results for
its calls were dropped, and the calls stay open. Nothing says they were
cancelled, so none is reported. Delivering those results would end them.
- A subagent that a SendMessage resumed: its calls carry the id of the
call that launched it, while the registry holds the SendMessage id.
`runsInBackground` has the same limit.
- The calls of a cycle that Claude Code started on its own are #1339.
2026-10-14 11:45:36 +02:00
Ben Brandt
764f339c3e fix: end the terminal of a command whose tool call is closed without a result (#1335)
## Problem

A tool call that ends without a tool result is closed by a report that
only sets its status (`AcpToolCallRenderer.unfinished`). That happens
after `session/cancel`, for a subagent's command (Claude Code sends no
result for it) or a call that Claude abandoned, and when a turn ends
with a call that has no result (`closeUnfinished` in `settleActive`).

A client that renders terminals (the `terminal_output` /
`terminal_output_delta` extension, and the display terminals of ACP v2)
holds a terminal for a Bash command, which its `tool_call` started with
`_meta.terminal_info`. The closing report did not end it, so the call
shows as ended next to a terminal that never exits.

Example, live (Claude Code 2.1.296, Sonnet): a foreground Agent call
whose subagent runs `python3 -c "import time; time.sleep(45)"`, and
`session/cancel` while it runs. Claude Code answers the Agent call and
sends nothing for the subagent's command, so the adapter closes that
call itself:

- v1 client with `_meta.terminal_output`: `tool_call_update` `failed`,
with no `terminal_exit`.
- v2 client: `terminal_update` with the command, then `tool_call_update`
`cancelled`, and no `terminal_update` that carries an exit.

## Change

- `closeToolCall` is the one function that every closing report goes
through (the cancelled-turn closing and the end-of-turn closing today,
and any closing added later). It now also ends the call's terminal, in
the same update, as the result of a command does. It reads the tool from
the tool use cache before it drops the entry.
- The closings that #1336 and #1339 add go through `closeToolCall` too,
so they end the terminal as well once merged.
- `AcpToolCallRenderer.terminalExit` decides whether the call has a
terminal. It uses the rule that `toolCall` uses to start one, a command
on a client that renders terminals, which is now one private method
(`hasTerminal`) that `toolCall`, `toolUseContent` and `terminalExit`
share.
- The exit code is unknown, since the command ended without a result:
`exit_code: null`, as `endBackgroundTerminal` sends for a background
command. AIR always gets a number and reads 1 for a command that did not
succeed (#1252), so it gets 1. A v2 client gets `terminal_update` with
an empty `exitStatus`.
- `docs/air-extensions.md`: one line in the terminal conventions.

## Evidence

**Tests** (8 new):

- `incomplete-tools.test.ts`: the cancel's closing of a subagent's
command ends its terminal, still names the subagent, and sends
`exit_code: null`; AIR gets `exit_code: 1`; a client without terminals
gets no exit; a subagent's `Read` gets none; a command that ran but got
no result by the end of the turn gets the exit; so does one that Claude
abandoned before it ran.
- `tools.test.ts`: `terminalExit` for Bash, PowerShell, v2, AIR, a
client without terminals, and tools that are not commands.
- `acp-v2.test.ts`: the stopped subagent's terminal exits in the child's
session, after the command's terminal started and before the root's
`idle cancelled`, and the call ends `cancelled`.

**Removal checks.** Each of these fails at least one test: no exit in
`closeToolCall` (5), AIR gets null (2), an exit for a client without
terminals (2), an exit for a tool that is not a command (3), another
terminal id (5), the exit replacing the parent meta (1), the tool read
after the cache entry is dropped (5).

**Live**, each update validated against the SDK's JSON schema (v1
`SessionNotification`, v2 `session/update`), 0 violations. The Agent
call and the subagent's command from the example, cancelled 4 s after
the command's permission request was answered:

| client | `main` | this PR |
| --- | --- | --- |
| v1, `_meta.terminal_output` | closing report `failed`, no
`terminal_exit` | the same update carries `terminal_exit {exit_code:
null, signal: null}` |
| v1, AIR with `terminal_output` | not run on `main` (same code path) |
`terminal_exit` with `exit_code: 1` |
| v2 | `cancelled`, no terminal update | `terminal_update` with
`exitStatus: {}`, then `cancelled`, before the root's `idle cancelled` |

`npm run check`, `npm run build` and `npm run test:run` (2932 tests)
pass. No golden file changes. With #1336 and #1339 merged into it, in
any of the six orders, the branch merges without conflicts and all 2948
tests pass.

## Not covered

- A call that only a permission request announced, whose tool use never
streamed before it was closed, has no entry in the tool use cache. Its
tool is unknown here, so its terminal stays open. This is the lookup
that `AsyncTaskRuntime` uses too (`toolUseCache`, then
`resolvedToolNames`). Covering it needs the tool name where the
permission request emits the call.
- The fallback for a tool result without a tool use
(`toAcpNotifications`) still reports the status only, with no exit.
- The end-of-turn closing is covered by a test only: Claude Code does
not end a turn with an unanswered call on demand.
2026-10-14 11:44:15 +02:00
Ben Brandt
ebf8b49fd6 fix: show nothing of a tool that cancelled work asks to run after the cancel was reported (#1334)
Closes #1061.

## Where #1061 stands

The incident in #1061 was tool starts streamed after `session/cancel`
that never got a final status, so a client's ledger of open tool calls
kept them forever. Since that report:

- **#1313** drops streamed text, thinking and tool calls of the stopped
work once the client has been told that the work ended (the cancelled
response on v1, `idle cancelled` on v2), while the window after the
cancel is open.
- **#1327** and **#1221** close the tool calls that streamed before that
report, before it is sent.

So a late streamed tool start no longer reaches the client. This PR adds
regression tests for that, and closes the one path that still produced
such a tool call. #1313 named it as not changed: a tool call that the
stopped work creates outside the stream.

## The remaining path

Example: the user sends a second prompt while the first runs, then
cancels. Claude Code had already taken the second prompt off its queue,
so it still runs it, and its answer writes a file. The consumer drops
the stream and the messages of that answer (#1313). But Claude Code asks
for the Write's permission on its control channel:

- **`canUseTool`** created the tool call (`ensureToolCallEmitted`) and
sent the permission request. AskUserQuestion's elicitation goes the same
way.
- **The v2 PreToolUse preview hook** created the tool call with the
Write's patch.

The tool's result is dropped too, so that tool call never ended: the
ghost of #1061, now for tools that need a permission.

## The fix

- **The consumer records each tool use it reaches,** at its streamed
start or, for one that did not stream, at its complete message, with
whether it dropped it. The condition is the stream's existing one, now
`dropsStoppedWorkOutput`: the client was told that the stopped work
ended, and the window after the cancel is open.
- **`canUseTool` denies a dropped tool use without showing anything:**
no tool call, no permission request, no elicitation. The model gets "The
user cancelled this prompt." The **preview hook** sends nothing for it.
- **The control channel runs ahead of the consumer,** which is why #1313
left this open. While a cancel's window may be open, `canUseTool` and
the hook wait until the consumer reaches the tool use. Claude Code
streams the tool use before it asks, so the consumer is usually there
within a few ticks. Example: the next prompt's tool asks while the
session still looks cancelled, because the consumer has not reached that
prompt's echo yet. The wait sees the consumer reach the echo, which
closes the window, and then the tool use, which it shows, so the request
goes out.
- **The wait is bounded:** 2 s, or until Claude Code withdraws the
request. The stream never shows some tool uses, such as
SandboxNetworkAccess's, and those then go out as before.
- **Without a cancel, nothing changes,** not even timing: the check
answers synchronously and adds no tick before the permission request.

## Evidence

**Tests** (`src/tests/acp-agent.test.ts`, "output after a cancel";
`src/tests/acp-v2.test.ts`):

- An abandoned prompt's Write: nothing reaches the client, and the tool
is denied. Claude Code asks before the consumer reaches the tool use,
between its streamed start and its message, after its message, or for a
tool use that did not stream. The same on v2, with Claude Code running
the registered PreToolUse hooks and then `canUseTool`.
- The next prompt's tool, asked while the window still looks open: its
request goes out once the consumer reaches it.
- A tool use the stream never shows: the wait ends when the request is
withdrawn (before it asked, or while it waits), or after the bound, and
then the request goes out.
- **Mutation checks:** removing each of the 11 rules fails at least one
test. That covers the `canUseTool` check, the hook check, the wait,
waiting instead of denying, the stream record, the message record, the
stream's dropped flag, the abort exit, the aborted-signal guard, the
timeout, and the synchronous answer.

**Live, against Claude Code** (v1 and v2, `default` permission mode,
every update checked against the schema):

- After a cancel, the next prompt's Writes asked for their permissions
at once and ran. Nothing was denied, and no tool call stayed open; on v2
the turn went `idle cancelled`, `running`, `requires_action`, `running`,
`idle end_turn`. No schema violations.
- The abandoned prompt's race did not happen live: Claude Code folded
the queued prompt into the cancelled request, and the cancel stopped
both. #1313 could not reproduce it either. The tests follow the order a
direct CLI probe recorded for #1309.

`npm run check`, build and the full suite (2931 tests) pass, with no
golden changes.

## Not covered

- On CLIs without `request_marker_lists_v1`, the window still lasts
until the next prompt's echo, as before.
- A tool that runs without a permission, in `acceptEdits` or
`bypassPermissions` mode, still runs for an abandoned prompt. Nothing of
it reaches the client, since its output is dropped. Stopping such tools
would need the adapter to block tools it is not asked about, which this
PR does not do.
2026-10-14 11:42:35 +02:00
Ben Brandt
df21b3fb56 feat: restore a subagent from before a reload when SendMessage resumes it, for subagents RFD clients (#1332)
After `session/load` or `session/resume`, Claude Code can resume an
agent that ran before the reload: a SendMessage to it runs it again from
its transcript. The adapter did not know that agent, so it was never
exposed, and its activity was dropped. Now it runs in its session from
before, the agent id, for every subagents RFD client: v1 clients that
declare `subagents` and are not AIR, and every v2 client.

## What Claude Code sends

From a live run, after a resume in a new adapter process:

- `task_started` with the agent id, the **SendMessage** call's id as
`tool_use_id`, and the message as `prompt`.
- The resumed run's messages under `parent_tool_use_id` = the agent's
**original Agent launch id**, from before the reload (the `toolUseId` in
`subagents/agent-<id>.meta.json`), not the SendMessage id.
- The SendMessage result with `resumedAgentId`, then `task_updated` /
`task_notification` as usual.

The runtime found no child for the agent id, and a SendMessage call
establishes no parent, so the child waited for a parent that never came
and was withdrawn when it ended.

## What changes

- **The signal:** an agent that the runtime does not know, resumed by a
SendMessage call that it saw. A new launch never counts: its
`task_started` names an Agent or Task call.
- **`subagentLineage`** (`subagent-history.ts`) reads the agent and its
ancestors from the session's history, root first. The metadata names
each launch call but not who made it, so a launch's parent is the
session whose transcript has the call: the root's, or an agent's. That
is the rule the replay (#1328) uses, so live restore and replay agree.
An agent or ancestor the history cannot place gives an empty lineage.
- **The runtime** (`NativeSubagentRuntime.restore`) registers the agent
and its ancestors under their agent ids and their launch ids. Then it
announces, parent before child, each ancestor that the client was not
told about in this connection, as the RFD requires before the agent's
traffic. An ancestor is announced with no work state, since nothing says
that it runs. Then the agent, as `running`. From there the resume goes
through the existing path (#1321): the SendMessage text is the child's
`session_message`, and its end is `idle`.
- **Unchanged:** an agent the history cannot place stays unexposed, as
the RFD forbids guessing a parent.
- **Load and replay:** after `session/load`, the replay has already
announced the agent. The live resume announces it once more, as
`running`, which repeats an association the client has (the RFD allows
it).
- **Refactor:** `subagentLaunchIds` moves from `acp-agent.ts` to
`subagent-history.ts`, so the replay and the lineage find launches the
same way.

## Evidence

**Live, against Claude Code, with every update checked against the
schema** (no violations):

| Run | Result |
|---|---|
| v1, `session/resume`, SendMessage to a root-level agent |
`subagent_update` running under its old id, the message from the root,
its `PING` in its own session, `idle end_turn` |
| v1, `session/load`, same | The replay announces it; the live resume
after the response runs in the same session |
| v1, `session/resume`, root → coordinator → echoer, SendMessage from
the root to echoer | coordinator announced under the root with no state,
then echoer under coordinator as running, its run in its session, its
`idle` on coordinator |
| v2, `session/resume` without replay, same nested session | as above;
echoer also reports `running` and `idle` as its own `state_update`,
coordinator has none |

Before this change, the first run showed no `subagent_update` at all,
and the child's `PING` was dropped.

**Tests:**

- `native-subagents.test.ts`: restore under the agent id, with routing
by the launch id; ancestors first and with no state; an ancestor
announced once for two agents; output that waited is delivered; restore
at the SendMessage result alone; an agent the history cannot place or
read stays unexposed; no history read for a new launch or a known child.
- `subagent-history.test.ts` (real transcripts on disk): a root-level
agent, a nested agent next to a sibling, an agent whose launch no
transcript has, a nested agent whose ancestor no transcript placed, an
agent without metadata.
- `acp-agent.test.ts`: the agent's own runtime restores an agent from a
history on disk, from the stream Claude Code sends.
- **Mutation checks:** removing each of the 12 rules fails at least one
test.

`npm run check`, build and the full suite (2922 tests) pass, with no
golden changes.

## Not covered

- **AIR** is unchanged: this PR changes only the RFD form, so on AIR an
agent from before a reload still stays unexposed.
- **A fork** cannot resume an agent of the original session, since the
SDK's `forkSession` copies no `subagents/` history.
2026-10-13 19:09:07 +02:00
acp-release-bot[bot]
7f7a1404a1 chore(main): release 0.90.0 (#1302)
🤖 I have created a release *beep* *boop*
---


##
[0.90.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.89.1...v0.90.0)
(2026-10-13)


### Features

* accept IDE session hooks without dropping model settings
([#1265](https://github.com/agentclientprotocol/claude-agent-acp/issues/1265))
([fe8b69e](fe8b69edd4))
* **deps:** Bump actions/setup-node from 7.0.0 to 7.1.0
([#1329](https://github.com/agentclientprotocol/claude-agent-acp/issues/1329))
([c6510c2](c6510c27c1))
* keep a subagent's session across SendMessage resumes for subagents RFD
clients
([#1321](https://github.com/agentclientprotocol/claude-agent-acp/issues/1321))
([bd06804](bd06804b67))
* replay each subagent under its live session id for subagents RFD
clients
([#1328](https://github.com/agentclientprotocol/claude-agent-acp/issues/1328))
([21eef57](21eef57655))
* report a subagent as requires_action while its request is open for
subagents RFD clients
([#1322](https://github.com/agentclientprotocol/claude-agent-acp/issues/1322))
([252126f](252126f616))
* report subagents as the subagents RFD defines, for clients that are
not AIR
([#1320](https://github.com/agentclientprotocol/claude-agent-acp/issues/1320))
([5758ac8](5758ac84a4))


### Bug Fixes

* close the open tool calls of a held or settling turn that a cancel
ends
([#1221](https://github.com/agentclientprotocol/claude-agent-acp/issues/1221))
([32c23db](32c23db06d))
* display active Central helpers as JetBrains Air Gateway
([#1266](https://github.com/agentclientprotocol/claude-agent-acp/issues/1266))
([d6c3717](d6c371704f))
* drop streamed output of prompts a cancel abandoned
([#1313](https://github.com/agentclientprotocol/claude-agent-acp/issues/1313))
([1137df3](1137df33aa))
* end a cancelled turn at its interrupted result and deliver Claude
Code's follow-up after a cancel
([#1309](https://github.com/agentclientprotocol/claude-agent-acp/issues/1309))
([b007ac7](b007ac7c45))
* end the tool calls of a cancelled turn before it reports the cancel
([#1327](https://github.com/agentclientprotocol/claude-agent-acp/issues/1327))
([53df1c7](53df1c7b22))
* keep the terminal of a command in the background running until its
task ends
([#1312](https://github.com/agentclientprotocol/claude-agent-acp/issues/1312))
([d074558](d074558e4a))
* report a command whose result says interrupted as cancelled on the
experimental ACP v2 surface
([#1301](https://github.com/agentclientprotocol/claude-agent-acp/issues/1301))
([b3bed2f](b3bed2f763))
* route a subagent's PostToolUse hook update to its child session
([#1324](https://github.com/agentclientprotocol/claude-agent-acp/issues/1324))
([4615a91](4615a9141c))
* start a forked session so the client can prompt it at once
([#1317](https://github.com/agentclientprotocol/claude-agent-acp/issues/1317))
([383bc7d](383bc7dcf0))

---
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-10-13 18:54:08 +02:00
dependabot[bot]
d6f60f512a feat(deps): Bump @anthropic-ai/claude-agent-sdk to 0.3.296 (#1330)
Bumps the minor group with 14 updates:

| Package | From | To |
| --- | --- | --- |
|
[@anthropic-ai/claude-agent-sdk](https://github.com/anthropics/claude-agent-sdk-typescript)
| `0.3.293` | `0.3.296` |
|
[@anthropic-ai/sdk](https://github.com/anthropics/anthropic-sdk-typescript)
| `0.132.1` | `0.133.0` |
|
[@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)
| `26.6.4` | `26.6.5` |
| [prettier](https://github.com/prettier/prettier) | `3.9.9` | `3.9.10`
|
| @anthropic-ai/claude-agent-sdk-darwin-arm64 | `0.3.293` | `0.3.296` |
| @anthropic-ai/claude-agent-sdk-darwin-x64 | `0.3.293` | `0.3.296` |
| @anthropic-ai/claude-agent-sdk-linux-arm64 | `0.3.293` | `0.3.296` |
| @anthropic-ai/claude-agent-sdk-linux-arm64-musl | `0.3.293` |
`0.3.296` |
| @anthropic-ai/claude-agent-sdk-linux-x64 | `0.3.293` | `0.3.296` |
| @anthropic-ai/claude-agent-sdk-linux-x64-musl | `0.3.293` | `0.3.296`
|
| @anthropic-ai/claude-agent-sdk-win32-arm64 | `0.3.293` | `0.3.296` |
| @anthropic-ai/claude-agent-sdk-win32-x64 | `0.3.293` | `0.3.296` |
| [express](https://github.com/expressjs/express) | `5.2.1` | `5.3.0` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) |
`8.3.3` | `8.3.4` |

Updates `@anthropic-ai/claude-agent-sdk` from 0.3.293 to 0.3.296
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/anthropics/claude-agent-sdk-typescript/releases">@​anthropic-ai/claude-agent-sdk's
releases</a>.</em></p>
<blockquote>
<h2>v0.3.296</h2>
<h2>What's changed</h2>
<ul>
<li>Added <code>claude_code_version</code> to the
<code>initialize</code> control response so clients know the CLI version
before the first turn</li>
<li>Added <code>autoCompactWindow</code> to
<code>AgentDefinition</code>, so a subagent can auto-compact earlier
than the main conversation's window</li>
<li>Fixed the <code>sandbox</code> option discarding an inline
<code>settings.sandbox</code> block: the two now merge, the option's
values win, and deny and credential lists from both sides are
combined</li>
<li>Changed the default limit on MCP tool descriptions sent up front and
on MCP server instructions, in-process SDK servers included, from 2,048
to 4,096 characters</li>
<li>Changed permission answers that arrive after a restart to be parsed
like live ones: over 4,096 <code>updatedPermissions</code> entries count
as a denial, and a malformed list is ignored as a whole</li>
<li>Updated to parity with Claude Code v2.1.296</li>
</ul>
<h2>Update</h2>
<pre lang="sh"><code>npm install @anthropic-ai/claude-agent-sdk@0.3.296
# or
yarn add @anthropic-ai/claude-agent-sdk@0.3.296
# or
pnpm add @anthropic-ai/claude-agent-sdk@0.3.296
# or
bun add @anthropic-ai/claude-agent-sdk@0.3.296
</code></pre>
<h2>v0.3.295</h2>
<h2>What's changed</h2>
<ul>
<li>Added <code>overageEnabled</code> to <code>SDKRateLimitInfo</code>:
on usage-limit warnings, whether the account has extra usage turned
on</li>
<li>Fixed an MCP server added with <code>mcp_set_servers</code>
sometimes keeping an outdated tool list when another server in the same
request connected later</li>
<li>Fixed assistant text blocks losing their <code>citations</code> in
streamed responses</li>
<li>Fixed Claude being told &quot;disabled by the user&quot; about an
in-process MCP server that was switched off with
<code>toggleMcpServer()</code></li>
<li>Fixed <code>rate_limit_event</code> with status
<code>allowed_warning</code> omitting <code>overageStatus</code>,
<code>overageResetsAt</code> and <code>overageDisabledReason</code>,
which <code>allowed</code> and <code>rejected</code> events already
carry</li>
<li>Changed MCP tool descriptions the model loads through tool search,
including those from in-process SDK servers, to be cut at 16,384
characters instead of 2,048</li>
<li>Changed a permission answer whose <code>updatedPermissions</code>
holds over 4,096 updates, rules and directories to count as a
denial</li>
<li>Changed <code>tool_result_meta[].non_execution_kind</code> to be
absent when a mod withheld the result of a tool that ran without error;
it was <code>permission-rule</code></li>
<li>Changed <code>pasted_content</code> on user messages to be ignored
when it holds over 1,000 entries and blocks in all</li>
<li>Changed how the SDK passes option values to Claude Code: the values
of its named options are now sent in the same argument as their flag
(<code>--flag=value</code>)</li>
<li>Changed <code>tool_use_result</code> for MCP Apps tools:
<code>structuredContent</code> or <code>_meta</code> over 8,388,608 JSON
characters is left off, and <code>_meta.ui</code> with no
<code>ui://</code> resource no longer lifts the ordinary caps</li>
<li>Changed <code>tool_use_result</code> for MCP tools: when a long
result is saved to a file, or cut because it could not be, the text kept
past 500,000 characters is dropped, with a note of how much</li>
<li>Changed <code>tool_use_result</code> for MCP tools: a result
<code>_meta</code> over 1,048,576 JSON characters is left off, except
for SDK-server tools; so is text blocks' <code>_meta</code> that totals
over 65,536</li>
<li>Updated to parity with Claude Code v2.1.295</li>
</ul>
<h2>Update</h2>
<pre lang="sh"><code>npm install @anthropic-ai/claude-agent-sdk@0.3.295
# or
yarn add @anthropic-ai/claude-agent-sdk@0.3.295
# or
pnpm add @anthropic-ai/claude-agent-sdk@0.3.295
# or
bun add @anthropic-ai/claude-agent-sdk@0.3.295
&lt;/tr&gt;&lt;/table&gt; 
</code></pre>
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/anthropics/claude-agent-sdk-typescript/blob/main/CHANGELOG.md">@​anthropic-ai/claude-agent-sdk's
changelog</a>.</em></p>
<blockquote>
<h2>0.3.296</h2>
<ul>
<li>Added <code>claude_code_version</code> to the
<code>initialize</code> control response so clients know the CLI version
before the first turn</li>
<li>Added <code>autoCompactWindow</code> to
<code>AgentDefinition</code>, so a subagent can auto-compact earlier
than the main conversation's window</li>
<li>Fixed the <code>sandbox</code> option discarding an inline
<code>settings.sandbox</code> block: the two now merge, the option's
values win, and deny and credential lists from both sides are
combined</li>
<li>Changed the default limit on MCP tool descriptions sent up front and
on MCP server instructions, in-process SDK servers included, from 2,048
to 4,096 characters</li>
<li>Changed permission answers that arrive after a restart to be parsed
like live ones: over 4,096 <code>updatedPermissions</code> entries count
as a denial, and a malformed list is ignored as a whole</li>
<li>Updated to parity with Claude Code v2.1.296</li>
</ul>
<h2>0.3.295</h2>
<ul>
<li>Added <code>overageEnabled</code> to <code>SDKRateLimitInfo</code>:
on usage-limit warnings, whether the account has extra usage turned
on</li>
<li>Fixed an MCP server added with <code>mcp_set_servers</code>
sometimes keeping an outdated tool list when another server in the same
request connected later</li>
<li>Fixed assistant text blocks losing their <code>citations</code> in
streamed responses</li>
<li>Fixed Claude being told &quot;disabled by the user&quot; about an
in-process MCP server that was switched off with
<code>toggleMcpServer()</code></li>
<li>Fixed <code>rate_limit_event</code> with status
<code>allowed_warning</code> omitting <code>overageStatus</code>,
<code>overageResetsAt</code> and <code>overageDisabledReason</code>,
which <code>allowed</code> and <code>rejected</code> events already
carry</li>
<li>Changed MCP tool descriptions the model loads through tool search,
including those from in-process SDK servers, to be cut at 16,384
characters instead of 2,048</li>
<li>Changed a permission answer whose <code>updatedPermissions</code>
holds over 4,096 updates, rules and directories to count as a
denial</li>
<li>Changed <code>tool_result_meta[].non_execution_kind</code> to be
absent when a mod withheld the result of a tool that ran without error;
it was <code>permission-rule</code></li>
<li>Changed <code>pasted_content</code> on user messages to be ignored
when it holds over 1,000 entries and blocks in all</li>
<li>Changed how the SDK passes option values to Claude Code: the values
of its named options are now sent in the same argument as their flag
(<code>--flag=value</code>)</li>
<li>Changed <code>tool_use_result</code> for MCP Apps tools:
<code>structuredContent</code> or <code>_meta</code> over 8,388,608 JSON
characters is left off, and <code>_meta.ui</code> with no
<code>ui://</code> resource no longer lifts the ordinary caps</li>
<li>Changed <code>tool_use_result</code> for MCP tools: when a long
result is saved to a file, or cut because it could not be, the text kept
past 500,000 characters is dropped, with a note of how much</li>
<li>Changed <code>tool_use_result</code> for MCP tools: a result
<code>_meta</code> over 1,048,576 JSON characters is left off, except
for SDK-server tools; so is text blocks' <code>_meta</code> that totals
over 65,536</li>
<li>Updated to parity with Claude Code v2.1.295</li>
</ul>
<h2>0.3.294</h2>
<ul>
<li>Updated to parity with Claude Code v2.1.294</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="85d8f8e077"><code>85d8f8e</code></a>
chore: Update CHANGELOG.md</li>
<li><a
href="da321bbe62"><code>da321bb</code></a>
chore: Update CHANGELOG.md</li>
<li><a
href="799dc78749"><code>799dc78</code></a>
chore: Update CHANGELOG.md</li>
<li>See full diff in <a
href="https://github.com/anthropics/claude-agent-sdk-typescript/compare/v0.3.293...v0.3.296">compare
view</a></li>
</ul>
</details>
<br />

Updates `@anthropic-ai/sdk` from 0.132.1 to 0.133.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/anthropics/anthropic-sdk-typescript/releases">@​anthropic-ai/sdk's
releases</a>.</em></p>
<blockquote>
<h2>sdk-v0.133.0</h2>
<h3>Features</h3>
<ul>
<li><strong>api:</strong> add types for the Chat and Cowork unified
analytics metrics</li>
<li><strong>api:</strong> add workflows, multiagent configuration and
thread status filtering to Managed Agents</li>
</ul>
<h3>Chores</h3>
<ul>
<li><strong>internal:</strong> remove the next branch rule from
CLAUDE.md</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/anthropics/anthropic-sdk-typescript/blob/main/CHANGELOG.md">@​anthropic-ai/sdk's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/anthropics/anthropic-sdk-typescript/compare/sdk-v0.132.1...sdk-v0.133.0">0.133.0</a>
(2026-10-09)</h2>
<h3>Features</h3>
<ul>
<li><strong>api:</strong> add types for the Chat and Cowork unified
analytics metrics</li>
<li><strong>api:</strong> add workflows, multiagent configuration and
thread status filtering to Managed Agents</li>
</ul>
<h3>Chores</h3>
<ul>
<li><strong>internal:</strong> remove the next branch rule from
CLAUDE.md</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="2979ed5cef"><code>2979ed5</code></a>
release: . 0.133.0</li>
<li><a
href="8486d9bd22"><code>8486d9b</code></a>
Merge pull request <a
href="https://redirect.github.com/anthropics/anthropic-sdk-typescript/issues/908">#908</a>
from anthropics/synced/published</li>
<li><a
href="e0995e4bde"><code>e0995e4</code></a>
feat(api): add workflows, multiagent configuration and thread status
filterin...</li>
<li><a
href="31eb3bea68"><code>31eb3be</code></a>
codegen metadata</li>
<li><a
href="5127b320bd"><code>5127b32</code></a>
codegen metadata</li>
<li><a
href="f702a46d87"><code>f702a46</code></a>
codegen metadata</li>
<li><a
href="d64e976b4e"><code>d64e976</code></a>
codegen metadata</li>
<li><a
href="9f754653fd"><code>9f75465</code></a>
codegen metadata</li>
<li><a
href="6ec2ea17ac"><code>6ec2ea1</code></a>
chore(internal): remove the next branch rule from CLAUDE.md</li>
<li><a
href="373c33dedf"><code>373c33d</code></a>
codegen metadata</li>
<li>Additional commits viewable in <a
href="https://github.com/anthropics/anthropic-sdk-typescript/compare/sdk-v0.132.1...sdk-v0.133.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `@types/node` from 26.6.4 to 26.6.5
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare
view</a></li>
</ul>
</details>
<br />

Updates `prettier` from 3.9.9 to 3.9.10
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/prettier/prettier/releases">prettier's
releases</a>.</em></p>
<blockquote>
<h2>3.9.10</h2>
<ul>
<li>Markdown: Update <code>micromark-extension-gfm-table</code> to fix
the table parse issue on performance (<a
href="https://redirect.github.com/prettier/prettier/pull/20266">#20266</a>
by <a href="https://github.com/fisker"><code>@​fisker</code></a>)</li>
</ul>
<p>🔗 <a
href="https://github.com/prettier/prettier/blob/3.9.10/CHANGELOG.md#3910">Changelog</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/prettier/prettier/blob/main/CHANGELOG.md">prettier's
changelog</a>.</em></p>
<blockquote>
<h1>3.9.10</h1>
<p><a
href="https://github.com/prettier/prettier/compare/3.9.9...3.9.10">diff</a></p>
<h4>Markdown: Update <code>micromark-extension-gfm-table</code> to fix
the table parse issue on performance (<a
href="https://redirect.github.com/prettier/prettier/pull/20266">#20266</a>
by <a href="https://github.com/fisker"><code>@​fisker</code></a>)</h4>
<p>See <a
href="4f43ead37b</a>
for details.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="1172b82c29"><code>1172b82</code></a>
Release 3.9.10</li>
<li><a
href="d0142273af"><code>d014227</code></a>
Update <code>micromark-extension-gfm-table</code> to fix the table parse
issue on perfor...</li>
<li><a
href="953b908432"><code>953b908</code></a>
Bump Prettier dependency to 3.9.9</li>
<li><a
href="ad8a6bf4fa"><code>ad8a6bf</code></a>
Clean changelog_unreleased</li>
<li>See full diff in <a
href="https://github.com/prettier/prettier/compare/3.9.9...3.9.10">compare
view</a></li>
</ul>
</details>
<br />

Updates `@anthropic-ai/claude-agent-sdk-darwin-arm64` from 0.3.293 to
0.3.296

Updates `@anthropic-ai/claude-agent-sdk-darwin-x64` from 0.3.293 to
0.3.296

Updates `@anthropic-ai/claude-agent-sdk-linux-arm64` from 0.3.293 to
0.3.296

Updates `@anthropic-ai/claude-agent-sdk-linux-arm64-musl` from 0.3.293
to 0.3.296

Updates `@anthropic-ai/claude-agent-sdk-linux-x64` from 0.3.293 to
0.3.296

Updates `@anthropic-ai/claude-agent-sdk-linux-x64-musl` from 0.3.293 to
0.3.296

Updates `@anthropic-ai/claude-agent-sdk-win32-arm64` from 0.3.293 to
0.3.296

Updates `@anthropic-ai/claude-agent-sdk-win32-x64` from 0.3.293 to
0.3.296

Updates `express` from 5.2.1 to 5.3.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/expressjs/express/releases">express's
releases</a>.</em></p>
<blockquote>
<h2>v5.3.0</h2>
<h2>Highlights</h2>
<h3>🐞 Bug fixes</h3>
<ul>
<li>
<p>Fixed HTTP header conflict between Content-Length and
Transfer-Encoding in res.send - by <a
href="https://github.com/YuryShkoda"><code>@​YuryShkoda</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/4893">#4893</a></p>
<p>Fixed the behavior of <code>res.send()</code> to prevent conflicts
between <code>Content-Length</code> and <code>Transfer-Encoding</code>
HTTP headers in responses. The <code>Content-Length</code> header in
<code>res.send()</code> is now only added when a
<code>Transfer-Encoding</code> header is not present, complying with the
HTTP specification that states both headers should not coexist in the
same response. ETag generation is unaffected by the presence of a
<code>Transfer-Encoding</code> header - by <a
href="https://github.com/cuishuang"><code>@​cuishuang</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/7459">#7459</a></p>
</li>
</ul>
<ul>
<li>
<p>Upgrade <code>qs</code> to <code>^6.16.0</code>, which fixes <a
href="https://www.cve.org/CVERecord?id=CVE-2026-2391">CVE-2026-2391</a>
(<a
href="https://github.com/ljharb/qs/security/advisories/GHSA-w7fw-mjwx-w883">GHSA-w7fw-mjwx-w883</a>),
<a
href="https://www.cve.org/CVERecord?id=CVE-2026-82417">CVE-2026-82417</a>
(<a
href="https://github.com/advisories/GHSA-4mjr-xmp4-gh2g">GHSA-4mjr-xmp4-gh2g</a>)
and <a
href="https://www.cve.org/CVERecord?id=CVE-2026-82562">CVE-2026-82562</a>
(<a
href="https://github.com/advisories/GHSA-x5fp-wj9c-mxmx">GHSA-x5fp-wj9c-mxmx</a>):
<code>arrayLimit</code> bypasses in comma parsing and a denial of
service via an attacker-controlled <code>isBuffer</code> - by <a
href="https://github.com/davetashner"><code>@​davetashner</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/7057">#7057</a>
and <a
href="https://redirect.github.com/expressjs/express/pull/7478">#7478</a>,
and <a
href="https://github.com/cyphercodes"><code>@​cyphercodes</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/7305">#7305</a></p>
</li>
<li>
<p>Upgrade <code>proxy-addr</code> to <code>^2.0.8</code>, which fixes
<a
href="https://www.cve.org/CVERecord?id=CVE-2026-90711">CVE-2026-90711</a>
- by <a href="https://github.com/lazerg"><code>@​lazerg</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/7474">#7474</a></p>
</li>
</ul>
<h3>🚀 Improvements</h3>
<ul>
<li>
<p>Allow conditional revalidation for QUERY requests.
<code>req.fresh</code> previously only validated freshness for GET and
HEAD requests, so QUERY responses never returned 304 despite a matching
validator. Since QUERY is a safe, idempotent, and cacheable method that
supports conditional requests, it is now included in the freshness check
- by <a href="https://github.com/Cherry"><code>@​Cherry</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/7366">#7366</a></p>
<pre lang="js"><code>// QUERY /reports with If-None-Match:
&quot;12345&quot;
app.query('/reports', (req, res) =&gt; {
  res.set('ETag', '&quot;12345&quot;');
  res.send(results); // now responds 304 Not Modified
});
</code></pre>
</li>
<li>
<p>Improve HTML structure in <code>res.redirect()</code> responses when
HTML format is accepted by adding <code>&lt;!DOCTYPE html&gt;</code>,
<code>&lt;title&gt;</code>, and <code>&lt;body&gt;</code> tags for
better browser compatibility - by <a
href="https://github.com/Bernice55231"><code>@​Bernice55231</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5167">#5167</a></p>
</li>
<li>
<p>When calling <code>app.render</code> with options set to null, the
locals object is handled correctly, preventing unexpected errors and
making the method behave the same as when options is omitted or an empty
object is passed - by <a
href="https://github.com/AkaHarshit">AkaHarshit</a> in <a
href="https://redirect.github.com/expressjs/express/pull/6903">#6903</a></p>
<pre lang="js"><code>app.render('index', null, callback); // now works
as expected
</code></pre>
</li>
<li>
<p>Upgrade <code>content-type</code> to <code>^2.0.0</code>, bringing a
faster parser (~1.5x quicker <code>Content-Type</code>
parsing/formatting in <code>res.send()</code>) along with a behavior
change: <code>res.send()</code> now keeps any existing parameters when
adding the charset and no longer throws on a <code>Content-Type</code>
that fails to parse. <code>type-is</code> is upgraded to
<code>^2.1.0</code> as part of the same change - by <a
href="https://github.com/blakeembrey"><code>@​blakeembrey</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/7234">#7234</a></p>
<pre lang="js"><code>res.set('Content-Type', 'text/plain;
foo=bar').send('hey');
// -&gt; Content-Type: text/plain; foo=bar; charset=utf-8
</code></pre>
</li>
<li>
<p>The default error handler now logs the full error object instead of
only its stack trace, so nested details such as <code>Error.cause</code>
and library-specific properties (e.g. Sequelize's
<code>parent</code>/<code>original</code>) are no longer swallowed - by
<a
href="https://github.com/Nitin-Mohapatra"><code>@​Nitin-Mohapatra</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/6464">#6464</a></p>
</li>
<li>
<p>Upgrade <code>content-disposition</code> to <code>^2.0.1</code>,
which changes the <code>Content-Disposition</code> header emitted by
<code>res.download()</code>, <code>res.attachment()</code>, and
<code>res.sendFile()</code>: file names that are valid HTTP tokens are
no longer wrapped in quotes. This is equivalent per RFC 6266, but
applications asserting on the exact header bytes should update their
expectations - by <a
href="https://github.com/blakeembrey"><code>@​blakeembrey</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/7233">#7233</a></p>
<pre lang="js"><code>res.attachment('user.html');
// before -&gt; Content-Disposition: attachment;
filename=&quot;user.html&quot;
// after  -&gt; Content-Disposition: attachment; filename=user.html
</code></pre>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/expressjs/express/blob/master/History.md">express's
changelog</a>.</em></p>
<blockquote>
<h1>5.3.0</h1>
<h2>🐞 Bug fixes</h2>
<ul>
<li>
<p>Fixed HTTP header conflict between Content-Length and
Transfer-Encoding in res.send - by <a
href="https://github.com/YuryShkoda"><code>@​YuryShkoda</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/4893">#4893</a></p>
<p>Fixed the behavior of <code>res.send()</code> to prevent conflicts
between <code>Content-Length</code> and <code>Transfer-Encoding</code>
HTTP headers in responses. The <code>Content-Length</code> header in
<code>res.send()</code> is now only added when a
<code>Transfer-Encoding</code> header is not present, complying with the
HTTP specification that states both headers should not coexist in the
same response. ETag generation is unaffected by the presence of a
<code>Transfer-Encoding</code> header - by <a
href="https://github.com/cuishuang"><code>@​cuishuang</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/7459">#7459</a></p>
</li>
</ul>
<ul>
<li>
<p>Upgrade <code>qs</code> to <code>^6.16.0</code>, which fixes <a
href="https://www.cve.org/CVERecord?id=CVE-2026-2391">CVE-2026-2391</a>
(<a
href="https://github.com/ljharb/qs/security/advisories/GHSA-w7fw-mjwx-w883">GHSA-w7fw-mjwx-w883</a>),
<a
href="https://www.cve.org/CVERecord?id=CVE-2026-82417">CVE-2026-82417</a>
(<a
href="https://github.com/advisories/GHSA-4mjr-xmp4-gh2g">GHSA-4mjr-xmp4-gh2g</a>)
and <a
href="https://www.cve.org/CVERecord?id=CVE-2026-82562">CVE-2026-82562</a>
(<a
href="https://github.com/advisories/GHSA-x5fp-wj9c-mxmx">GHSA-x5fp-wj9c-mxmx</a>):
<code>arrayLimit</code> bypasses in comma parsing and a denial of
service via an attacker-controlled <code>isBuffer</code> - by <a
href="https://github.com/davetashner"><code>@​davetashner</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/7057">#7057</a>
and <a
href="https://redirect.github.com/expressjs/express/pull/7478">#7478</a>,
and <a
href="https://github.com/cyphercodes"><code>@​cyphercodes</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/7305">#7305</a></p>
</li>
<li>
<p>Upgrade <code>proxy-addr</code> to <code>^2.0.8</code>, which fixes
<a
href="https://www.cve.org/CVERecord?id=CVE-2026-90711">CVE-2026-90711</a>
- by <a href="https://github.com/lazerg"><code>@​lazerg</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/7474">#7474</a></p>
</li>
</ul>
<h2>🚀 Improvements</h2>
<ul>
<li>
<p>Allow conditional revalidation for QUERY requests.
<code>req.fresh</code> previously only validated freshness for GET and
HEAD requests, so QUERY responses never returned 304 despite a matching
validator. Since QUERY is a safe, idempotent, and cacheable method that
supports conditional requests, it is now included in the freshness check
- by <a href="https://github.com/Cherry"><code>@​Cherry</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/7366">#7366</a></p>
<pre lang="js"><code>// QUERY /reports with If-None-Match:
&quot;12345&quot;
app.query('/reports', (req, res) =&gt; {
  res.set('ETag', '&quot;12345&quot;');
  res.send(results); // now responds 304 Not Modified
});
</code></pre>
</li>
<li>
<p>Improve HTML structure in <code>res.redirect()</code> responses when
HTML format is accepted by adding <code>&lt;!DOCTYPE html&gt;</code>,
<code>&lt;title&gt;</code>, and <code>&lt;body&gt;</code> tags for
better browser compatibility - by <a
href="https://github.com/Bernice55231"><code>@​Bernice55231</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5167">#5167</a></p>
</li>
<li>
<p>When calling <code>app.render</code> with options set to null, the
locals object is handled correctly, preventing unexpected errors and
making the method behave the same as when options is omitted or an empty
object is passed - by <a
href="https://github.com/AkaHarshit">AkaHarshit</a> in <a
href="https://redirect.github.com/expressjs/express/pull/6903">#6903</a></p>
<pre lang="js"><code>app.render('index', null, callback); // now works
as expected
</code></pre>
</li>
<li>
<p>Upgrade <code>content-type</code> to <code>^2.0.0</code>, bringing a
faster parser (~1.5x quicker <code>Content-Type</code>
parsing/formatting in <code>res.send()</code>) along with a behavior
change: <code>res.send()</code> now keeps any existing parameters when
adding the charset and no longer throws on a <code>Content-Type</code>
that fails to parse. <code>type-is</code> is upgraded to
<code>^2.1.0</code> as part of the same change - by <a
href="https://github.com/blakeembrey"><code>@​blakeembrey</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/7234">#7234</a></p>
<pre lang="js"><code>res.set('Content-Type', 'text/plain;
foo=bar').send('hey');
// -&gt; Content-Type: text/plain; foo=bar; charset=utf-8
</code></pre>
</li>
<li>
<p>The default error handler now logs the full error object instead of
only its stack trace, so nested details such as <code>Error.cause</code>
and library-specific properties (e.g. Sequelize's
<code>parent</code>/<code>original</code>) are no longer swallowed - by
<a
href="https://github.com/Nitin-Mohapatra"><code>@​Nitin-Mohapatra</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/6464">#6464</a></p>
</li>
<li>
<p>Upgrade <code>content-disposition</code> to <code>^2.0.1</code>,
which changes the <code>Content-Disposition</code> header emitted by
<code>res.download()</code>, <code>res.attachment()</code>, and
<code>res.sendFile()</code>: file names that are valid HTTP tokens are
no longer wrapped in quotes. This is equivalent per RFC 6266, but
applications asserting on the exact header bytes should update their
expectations - by <a
href="https://github.com/blakeembrey"><code>@​blakeembrey</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/7233">#7233</a></p>
<pre lang="js"><code>res.attachment('user.html');
// before -&gt; Content-Disposition: attachment;
filename=&quot;user.html&quot;
// after  -&gt; Content-Disposition: attachment; filename=user.html
</code></pre>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="cf6722bf89"><code>cf6722b</code></a>
5.3.0 (<a
href="https://redirect.github.com/expressjs/express/issues/7468">#7468</a>)</li>
<li><a
href="8c2fcfcb7d"><code>8c2fcfc</code></a>
docs: fix typos and a stale API reference in lib comments (<a
href="https://redirect.github.com/expressjs/express/issues/7512">#7512</a>)</li>
<li><a
href="9efc29e280"><code>9efc29e</code></a>
docs: fix pass-through typos (<a
href="https://redirect.github.com/expressjs/express/issues/7509">#7509</a>)</li>
<li><a
href="2717ae3cbf"><code>2717ae3</code></a>
build(deps): bump the github-actions group with 3 updates (<a
href="https://redirect.github.com/expressjs/express/issues/7499">#7499</a>)</li>
<li><a
href="de777714df"><code>de77771</code></a>
build(deps-dev): bump hbs from 4.3.0 to 4.3.1 (<a
href="https://redirect.github.com/expressjs/express/issues/7500">#7500</a>)</li>
<li><a
href="9169cbe112"><code>9169cbe</code></a>
docs: add missing apostrophe in example (<a
href="https://redirect.github.com/expressjs/express/issues/7483">#7483</a>)</li>
<li><a
href="7ef98448f8"><code>7ef9844</code></a>
build(deps-dev): bump morgan from 1.12.0 to 1.12.1 (<a
href="https://redirect.github.com/expressjs/express/issues/7493">#7493</a>)</li>
<li><a
href="3b7e39fe74"><code>3b7e39f</code></a>
deps: bump qs minimum to 6.16.0 (<a
href="https://redirect.github.com/expressjs/express/issues/7478">#7478</a>)</li>
<li><a
href="98bd4cd96b"><code>98bd4cd</code></a>
deps: proxy-addr@^2.0.8 (<a
href="https://redirect.github.com/expressjs/express/issues/7474">#7474</a>)</li>
<li><a
href="9a34acf03c"><code>9a34acf</code></a>
fix(res.send): preserve ETag generation with Transfer-Encoding (<a
href="https://redirect.github.com/expressjs/express/issues/7459">#7459</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/expressjs/express/compare/v5.2.1...v5.3.0">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for express since your current version.</p>
</details>
<br />

Updates `vite` from 8.3.3 to 8.3.4
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/releases">vite's
releases</a>.</em></p>
<blockquote>
<h2>v8.3.4</h2>
<h3>Features</h3>
<ul>
<li><strong>bundled-dev:</strong> support
<code>import.meta.hot.acceptExports</code> (<a
href="https://redirect.github.com/vitejs/vite/issues/23463">#23463</a>)
(<a
href="bc0f21c56a">bc0f21c</a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li>avoid mutating hmr defaults (fix <a
href="https://redirect.github.com/vitejs/vite/issues/23575">#23575</a>)
(<a
href="https://redirect.github.com/vitejs/vite/issues/23576">#23576</a>)
(<a
href="574d5e3519">574d5e3</a>)</li>
<li><strong>build:</strong> correct preloads when chunkImportMap is
enabled with non-root base (<a
href="https://redirect.github.com/vitejs/vite/issues/23698">#23698</a>)
(<a
href="2a8c0f05c7">2a8c0f0</a>)</li>
<li><strong>build:</strong> wait for in-flight stylesheets in preload
helper (fix <a
href="https://redirect.github.com/vitejs/vite/issues/23652">#23652</a>)
(<a
href="https://redirect.github.com/vitejs/vite/issues/23662">#23662</a>)
(<a
href="036b74599b">036b745</a>)</li>
<li><strong>bundled-dev:</strong> serve <code>/@vite/client</code> and
stub <code>@vite/env</code> (<a
href="https://redirect.github.com/vitejs/vite/issues/23664">#23664</a>)
(<a
href="5afc5a8876">5afc5a8</a>)</li>
<li><strong>client:</strong> skip rewriting unchanged adopted styles (<a
href="https://redirect.github.com/vitejs/vite/issues/23697">#23697</a>)
(<a
href="fa7146938b">fa71469</a>)</li>
<li><strong>css:</strong> resolve imported preprocessors from their file
path with lightningcss (<a
href="https://redirect.github.com/vitejs/vite/issues/23688">#23688</a>)
(<a
href="cf52011c46">cf52011</a>)</li>
<li><strong>deps:</strong> update all non-major dependencies (<a
href="https://redirect.github.com/vitejs/vite/issues/23648">#23648</a>)
(<a
href="8a4c19cfc0">8a4c19c</a>)</li>
<li><strong>deps:</strong> update rolldown-related dependencies (<a
href="https://redirect.github.com/vitejs/vite/issues/23649">#23649</a>)
(<a
href="794516d50c">794516d</a>)</li>
<li><strong>dev:</strong> a restart requested during a restart used to
be dropped (fix <a
href="https://redirect.github.com/vitejs/vite/issues/23392">#23392</a>)
(<a
href="https://redirect.github.com/vitejs/vite/issues/23484">#23484</a>)
(<a
href="70d56ea184">70d56ea</a>)</li>
<li>don't treat modules ending with js-like query as js (<a
href="https://redirect.github.com/vitejs/vite/issues/23687">#23687</a>)
(<a
href="fcb6f1252a">fcb6f12</a>)</li>
<li><strong>html:</strong> avoid adding filesystem root to watcher (<a
href="https://redirect.github.com/vitejs/vite/issues/23689">#23689</a>)
(<a
href="3a7f2aea75">3a7f2ae</a>)</li>
<li><strong>html:</strong> skip caching CSS of chunks in a cycle (fix <a
href="https://redirect.github.com/vitejs/vite/issues/23628">#23628</a>)
(<a
href="https://redirect.github.com/vitejs/vite/issues/23636">#23636</a>)
(<a
href="15d82f7dc6">15d82f7</a>)</li>
<li><strong>optimizer:</strong> scan deep imports with custom extensions
(<a
href="https://redirect.github.com/vitejs/vite/issues/23678">#23678</a>)
(<a
href="8579189bae">8579189</a>)</li>
<li>remove input option unescaping for now (<a
href="https://redirect.github.com/vitejs/vite/issues/23694">#23694</a>)
(<a
href="facc2fb1ff">facc2fb</a>)</li>
<li><strong>server:</strong> match static aliases on path boundaries (<a
href="https://redirect.github.com/vitejs/vite/issues/23643">#23643</a>)
(<a
href="b28db28e9e">b28db28</a>)</li>
<li><strong>watcher:</strong> normalize file before root check in
<code>ensureWatchedFile</code> (<a
href="https://redirect.github.com/vitejs/vite/issues/23673">#23673</a>)
(<a
href="12bb28c223">12bb28c</a>)</li>
</ul>
<h3>Performance Improvements</h3>
<ul>
<li><strong>module-runner:</strong> skip cloning call sites that have no
source map (<a
href="https://redirect.github.com/vitejs/vite/issues/23646">#23646</a>)
(<a
href="3d674869a5">3d67486</a>)</li>
</ul>
<h3>Code Refactoring</h3>
<ul>
<li><strong>bundled-dev:</strong> import CSS HMR helpers from
<code>/@vite/client</code> (<a
href="https://redirect.github.com/vitejs/vite/issues/23674">#23674</a>)
(<a
href="182f5b2a65">182f5b2</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted --><a
href="https://github.com/vitejs/vite/compare/v8.3.3...v8.3.4">8.3.4</a>
(2026-10-08)<!-- raw HTML omitted --></h2>
<h3>Features</h3>
<ul>
<li><strong>bundled-dev:</strong> support
<code>import.meta.hot.acceptExports</code> (<a
href="https://redirect.github.com/vitejs/vite/issues/23463">#23463</a>)
(<a
href="bc0f21c56a">bc0f21c</a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li>avoid mutating hmr defaults (fix <a
href="https://redirect.github.com/vitejs/vite/issues/23575">#23575</a>)
(<a
href="https://redirect.github.com/vitejs/vite/issues/23576">#23576</a>)
(<a
href="574d5e3519">574d5e3</a>)</li>
<li><strong>build:</strong> correct preloads when chunkImportMap is
enabled with non-root base (<a
href="https://redirect.github.com/vitejs/vite/issues/23698">#23698</a>)
(<a
href="2a8c0f05c7">2a8c0f0</a>)</li>
<li><strong>build:</strong> wait for in-flight stylesheets in preload
helper (fix <a
href="https://redirect.github.com/vitejs/vite/issues/23652">#23652</a>)
(<a
href="https://redirect.github.com/vitejs/vite/issues/23662">#23662</a>)
(<a
href="036b74599b">036b745</a>)</li>
<li><strong>bundled-dev:</strong> serve <code>/@vite/client</code> and
stub <code>@vite/env</code> (<a
href="https://redirect.github.com/vitejs/vite/issues/23664">#23664</a>)
(<a
href="5afc5a8876">5afc5a8</a>)</li>
<li><strong>client:</strong> skip rewriting unchanged adopted styles (<a
href="https://redirect.github.com/vitejs/vite/issues/23697">#23697</a>)
(<a
href="fa7146938b">fa71469</a>)</li>
<li><strong>css:</strong> resolve imported preprocessors from their file
path with lightningcss (<a
href="https://redirect.github.com/vitejs/vite/issues/23688">#23688</a>)
(<a
href="cf52011c46">cf52011</a>)</li>
<li><strong>deps:</strong> update all non-major dependencies (<a
href="https://redirect.github.com/vitejs/vite/issues/23648">#23648</a>)
(<a
href="8a4c19cfc0">8a4c19c</a>)</li>
<li><strong>deps:</strong> update rolldown-related dependencies (<a
href="https://redirect.github.com/vitejs/vite/issues/23649">#23649</a>)
(<a
href="794516d50c">794516d</a>)</li>
<li><strong>dev:</strong> a restart requested during a restart used to
be dropped (fix <a
href="https://redirect.github.com/vitejs/vite/issues/23392">#23392</a>)
(<a
href="https://redirect.github.com/vitejs/vite/issues/23484">#23484</a>)
(<a
href="70d56ea184">70d56ea</a>)</li>
<li>don't treat modules ending with js-like query as js (<a
href="https://redirect.github.com/vitejs/vite/issues/23687">#23687</a>)
(<a
href="fcb6f1252a">fcb6f12</a>)</li>
<li><strong>html:</strong> avoid adding filesystem root to watcher (<a
href="https://redirect.github.com/vitejs/vite/issues/23689">#23689</a>)
(<a
href="3a7f2aea75">3a7f2ae</a>)</li>
<li><strong>html:</strong> skip caching CSS of chunks in a cycle (fix <a
href="https://redirect.github.com/vitejs/vite/issues/23628">#23628</a>)
(<a
href="https://redirect.github.com/vitejs/vite/issues/23636">#23636</a>)
(<a
href="15d82f7dc6">15d82f7</a>)</li>
<li><strong>optimizer:</strong> scan deep imports with custom extensions
(<a
href="https://redirect.github.com/vitejs/vite/issues/23678">#23678</a>)
(<a
href="8579189bae">8579189</a>)</li>
<li>remove input option unescaping for now (<a
href="https://redirect.github.com/vitejs/vite/issues/23694">#23694</a>)
(<a
href="facc2fb1ff">facc2fb</a>)</li>
<li><strong>server:</strong> match static aliases on path boundaries (<a
href="https://redirect.github.com/vitejs/vite/issues/23643">#23643</a>)
(<a
href="b28db28e9e">b28db28</a>)</li>
<li><strong>watcher:</strong> normalize file before root check in
<code>ensureWatchedFile</code> (<a
href="https://redirect.github.com/vitejs/vite/issues/23673">#23673</a>)
(<a
href="12bb28c223">12bb28c</a>)</li>
</ul>
<h3>Performance Improvements</h3>
<ul>
<li><strong>module-runner:</strong> skip cloning call sites that have no
source map (<a
href="https://redirect.github.com/vitejs/vite/issues/23646">#23646</a>)
(<a
href="3d674869a5">3d67486</a>)</li>
</ul>
<h3>Code Refactoring</h3>
<ul>
<li><strong>bundled-dev:</strong> import CSS HMR helpers from
<code>/@vite/client</code> (<a
href="https://redirect.github.com/vitejs/vite/issues/23674">#23674</a>)
(<a
href="182f5b2a65">182f5b2</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="b6c20f6a81"><code>b6c20f6</code></a>
release: v8.3.4 (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/23702">#23702</a>)</li>
<li><a
href="b28db28e9e"><code>b28db28</code></a>
fix(server): match static aliases on path boundaries (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/23643">#23643</a>)</li>
<li><a
href="bc0f21c56a"><code>bc0f21c</code></a>
feat(bundled-dev): support <code>import.meta.hot.acceptExports</code>
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/23463">#23463</a>)</li>
<li><a
href="fa7146938b"><code>fa71469</code></a>
fix(client): skip rewriting unchanged adopted styles (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/23697">#23697</a>)</li>
<li><a
href="2a8c0f05c7"><code>2a8c0f0</code></a>
fix(build): correct preloads when chunkImportMap is enabled with
non-root bas...</li>
<li><a
href="3a7f2aea75"><code>3a7f2ae</code></a>
fix(html): avoid adding filesystem root to watcher (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/23689">#23689</a>)</li>
<li><a
href="facc2fb1ff"><code>facc2fb</code></a>
fix: remove input option unescaping for now (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/23694">#23694</a>)</li>
<li><a
href="70d56ea184"><code>70d56ea</code></a>
fix(dev): a restart requested during a restart used to be dropped (fix
<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/23392">#23392</a>...</li>
<li><a
href="cf52011c46"><code>cf52011</code></a>
fix(css): resolve imported preprocessors from their file path with
lightningc...</li>
<li><a
href="fcb6f1252a"><code>fcb6f12</code></a>
fix: don't treat modules ending with js-like query as js (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/23687">#23687</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/vitejs/vite/commits/v8.3.4/packages/vite">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2026-10-13 16:41:54 +00:00
Demetrios Chiuratto Agourakis
32c23db06d fix: close the open tool calls of a held or settling turn that a cancel ends (#1221)
Refs #1061.

> Reworked on top of #1327, which merged in the meantime and already
closes a cancelled turn's open tool calls in the consumer. This PR now
covers the one path #1327 does not reach. The original description is
kept at the end.

## What it fixes

When a cancel lands on a turn that already has its result, `cancelTurns`
settles the turn itself instead of leaving it to the consumer:

- a turn held open for its background subagents, while the session is
idle;
- a turn waiting on its checkpoint preview.

That path skipped #1327's `closeCancelledToolCalls`, so a tool call
still open there stayed `pending` after the cancelled response.
`cancelTurns` now runs the same closing before it responds.

## Changes

- **One rule for both paths.** The consumer exposes its closing as
`Session.closeCancelledToolCalls`, the way it already exposes
`nativeSubagentDeliver`. Every open call ends with
`AcpToolCallRenderer.unfinished`, except a background subagent's calls,
which the cancel did not stop.
- **The closing runs once per turn** (`Turn.closingCancelledToolCalls`).
Example: a first cancel lands during a held turn's follow-up cycle, so
the consumer settles the turn at the idle and starts closing its calls.
A second cancel then settles the turn inline. It waits for the
consumer's closing instead of starting its own, so each call gets one
report and the response comes after all of them.
- **`cancelTurns`** marks the turn settled before it waits for the
reports, so the consumer cannot settle it again. If a report fails, it
logs the error and still responds and interrupts.
- **Removed:** this PR's original consumer-side branch and its `failed`
report, which #1327's closing replaces.

## Tests

- **Kept from this PR:** inline settles of a deferred turn and of a
checkpoint turn, overlapping and simultaneous cancels, a failed report,
a checkpoint that finishes during the closing.
- **Changed:** the deferred fixture's background shell had no tool
result, though Claude Code answers a `run_in_background` call when it
starts. It is now a background subagent whose own command stays open.
- **Removed:** `terminates a tool whose start streams in after the
cancel`, which #1327 covers.
- **New:** a second cancel that settles a held turn while the consumer
is closing its calls.
- **Removal checks:** removing the inline closing fails 6 tests, the
once-per-turn closing 1, settling before the wait 2, and the error
handling 1.

`npm run check`, build and the full suite (2909 tests) pass, with no
golden changes.

## Not covered

A tool start that streams in after the turn has settled (the incident in
#1061) still opens a tool call that nothing closes. Neither this PR nor
#1327 changes that.

<details>
<summary>Original description</summary>

Refs #1061. This closes part of the issue, not all of it; the part it
leaves open is described below.

## What it fixes

`settleActive` already fails every unfinished foreground tool when a
turn ends as `end_turn` ("Claude ended the turn without returning a
result for this tool"). It did nothing when the turn settled as
`cancelled`, so a tool start that never got a `tool_result` stayed in
`emittedToolCalls` and the client was left with an open `tool_call`
forever.

How a start can end up with no terminal: the `stream_event` case has no
`session.cancelled` gate, so `content_block_start` for a `tool_use`
keeps becoming a `tool_call` after `cancel()`. The consolidated
`user`/`assistant` path, which is where `tool_result`-derived terminals
come from, drops everything at its `session.cancelled` guard.

This adds a `cancelled` branch next to the `end_turn` one. It reuses the
same selection (this turn's `foregroundToolCallIds` still in
`emittedToolCalls`, minus confirmed background tasks) and sends one
`tool_call_update` with `status: "failed"` per tool ("The turn was
cancelled before this tool call finished."). ACP's `ToolCallStatus` has
no cancelled value, so `failed` is the available terminal. Unlike
`end_turn`, cancellation is expected, so there is no session failure and
no early return.

## What it does not fix

A tool start that arrives **after** the turn has already settled (the
reporter's incident: starts streaming in minutes after the cancel).
There is no unsettled turn to attach it to, and I could not find an
exact gate: stream events carry no `origin` or turn attribution (the
existing comment on `emittedAssistantText` says as much), so such a
start cannot be told apart from an autonomous cycle or from the next
turn's pre-echo events. Gating on `session.cancelled` has the inverse
problem the reporter described. If maintainers see an attribution signal
I missed, a lifecycle-scoped gate would be the complement to this
change.

## Test changes

- `preserves cancellation and does not attribute its unfinished tool to
the next turn` asserted that the cancelled turn's tool gets **no**
`failed` update. That encoded the old behaviour, so it now asserts
exactly one cancellation terminal that the next turn neither repeats nor
reports as `incomplete_tool_call`. The cancel-then-next-prompt
assertions are unchanged.
- New: `terminates a tool whose start streams in after the cancel`
(start arrives between `cancel()` and the trailing idle).

Both fail without the change and pass with it. Full suite (2127 tests),
`npm run check` and `build` pass. One unrelated timeout in
`acp-scenarios` (`schema check`) showed up under load in one full run
and passes when run on its own.

## Live check

Against the built adapter (real credentials), I asked for `sleep 45`,
cancelled once the `tool_call` appeared, and listed every tool call and
its statuses at the end. Before and after the change the result is the
same: one tool call, `pending > ... > failed`, nothing open. So in the
plain cancel-mid-tool case the SDK already delivers an interrupted
result and there is no ghost to remove; I could not reproduce the
reporter's leak live, only in the unit fixture above. What the live run
does show is that the new branch does not send a duplicate terminal for
a tool that was already terminated.

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

https://claude.ai/code/session_019vH6BXrsYQN3MqQ93JgStf


</details>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2026-10-13 18:36:39 +02:00
dependabot[bot]
c6510c27c1 feat(deps): Bump actions/setup-node from 7.0.0 to 7.1.0 (#1329)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from
7.0.0 to 7.1.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/setup-node/releases">actions/setup-node's
releases</a>.</em></p>
<blockquote>
<h2>v7.1.0</h2>
<h2>What's Changed</h2>
<h3>Enhancements:</h3>
<ul>
<li>Add support for mise.toml file by <a
href="https://github.com/gperdomor"><code>@​gperdomor</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1421">actions/setup-node#1421</a></li>
<li>Support absolute paths in node-version-file by <a
href="https://github.com/v-mahabaleshwars"><code>@​v-mahabaleshwars</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1633">actions/setup-node#1633</a></li>
</ul>
<h3>Bug fixes:</h3>
<ul>
<li>Add Node version validation and manifest fetch retry by <a
href="https://github.com/v-jitenderpalsingh"><code>@​v-jitenderpalsingh</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1580">actions/setup-node#1580</a></li>
</ul>
<h3>Documentation updates:</h3>
<ul>
<li>README.md: bump actions/checkout to latest version 7 by <a
href="https://github.com/deining"><code>@​deining</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1578">actions/setup-node#1578</a></li>
<li>docs: add V7 ESM migration and NODE_AUTH_TOKEN behavior change by <a
href="https://github.com/v-gowridurgad"><code>@​v-gowridurgad</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1593">actions/setup-node#1593</a></li>
<li>Fixed cache recommendation syntax by <a
href="https://github.com/SubJunk"><code>@​SubJunk</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1603">actions/setup-node#1603</a></li>
</ul>
<h3>Dependency updates:</h3>
<ul>
<li>Update workflow pins and migrate Node matrices to modern versions by
<a
href="https://github.com/v-HarithaVattikuti"><code>@​v-HarithaVattikuti</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1583">actions/setup-node#1583</a></li>
<li>Upgrade <code>@​actions/cache</code> to 6.2.0 by <a
href="https://github.com/philip-gai"><code>@​philip-gai</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1584">actions/setup-node#1584</a></li>
<li>Pin <code>@​types/node</code> to v24 by <a
href="https://github.com/v-priyagupta108"><code>@​v-priyagupta108</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1585">actions/setup-node#1585</a></li>
<li>Override brace-expansion to 5.0.8 by <a
href="https://github.com/v-priyagupta108"><code>@​v-priyagupta108</code></a>
with <a href="https://github.com/Copilot"><code>@​Copilot</code></a> in
<a
href="https://redirect.github.com/actions/setup-node/pull/1599">actions/setup-node#1599</a></li>
<li>Upgrade actions/checkout from 7.0.0 to 7.0.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1598">actions/setup-node#1598</a></li>
<li>Upgrade prettier from 3.8.4 to 3.9.6 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1590">actions/setup-node#1590</a></li>
<li>Upgrade <code>@​vercel/ncc</code> from 0.44.0 to 0.45.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1610">actions/setup-node#1610</a></li>
<li>Upgrade pnpm/action-setup from 6.0.9 to 6.0.10 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1602">actions/setup-node#1602</a></li>
<li>Upgrade baseline-browser-mapping from 2.10.38 to 2.11.22 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1629">actions/setup-node#1629</a></li>
<li>Upgrade <code>@​types/semver</code> from 7.7.1 to 7.8.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1616">actions/setup-node#1616</a></li>
<li>Upgrade <code>@​typescript-eslint/parser</code> from 8.62.0 to
8.70.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1615">actions/setup-node#1615</a></li>
<li>Upgrade pnpm/action-setup from 6.0.10 to 6.1.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1631">actions/setup-node#1631</a></li>
<li>Upgrade jest-each from 30.4.1 to 30.5.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1636">actions/setup-node#1636</a></li>
<li>Upgrade eslint-plugin-n from 18.1.0 to 18.3.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1638">actions/setup-node#1638</a></li>
<li>Update dependencies and add license files by <a
href="https://github.com/v-HarithaVattikuti"><code>@​v-HarithaVattikuti</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1642">actions/setup-node#1642</a></li>
<li>Upgrade eslint from 10.5.0 to 10.11.0 and package.json from 7.0.0 to
7.1.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1635">actions/setup-node#1635</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/v-jitenderpalsingh"><code>@​v-jitenderpalsingh</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1580">actions/setup-node#1580</a></li>
<li><a
href="https://github.com/philip-gai"><code>@​philip-gai</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1584">actions/setup-node#1584</a></li>
<li><a href="https://github.com/gperdomor"><code>@​gperdomor</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1421">actions/setup-node#1421</a></li>
<li><a href="https://github.com/SubJunk"><code>@​SubJunk</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1603">actions/setup-node#1603</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-node/compare/v7.0.0...v7.1.0">https://github.com/actions/setup-node/compare/v7.0.0...v7.1.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="949feb2413"><code>949feb2</code></a>
Bump eslint from 10.5.0 to 10.11.0 and package.json from 7.0.0 to 7.1.0
(<a
href="https://redirect.github.com/actions/setup-node/issues/1635">#1635</a>)</li>
<li><a
href="7207c4f118"><code>7207c4f</code></a>
chore: update dependencies and add license files (<a
href="https://redirect.github.com/actions/setup-node/issues/1642">#1642</a>)</li>
<li><a
href="54337e0d02"><code>54337e0</code></a>
Bump eslint-plugin-n from 18.1.0 to 18.3.0 (<a
href="https://redirect.github.com/actions/setup-node/issues/1638">#1638</a>)</li>
<li><a
href="fb54fa8c14"><code>fb54fa8</code></a>
Bump jest-each from 30.4.1 to 30.5.2 (<a
href="https://redirect.github.com/actions/setup-node/issues/1636">#1636</a>)</li>
<li><a
href="7f84f1d52d"><code>7f84f1d</code></a>
Bump pnpm/action-setup from 6.0.10 to 6.1.0 (<a
href="https://redirect.github.com/actions/setup-node/issues/1631">#1631</a>)</li>
<li><a
href="0c54c7b074"><code>0c54c7b</code></a>
Bump <code>@​typescript-eslint/parser</code> from 8.62.0 to 8.70.0 (<a
href="https://redirect.github.com/actions/setup-node/issues/1615">#1615</a>)</li>
<li><a
href="a829419004"><code>a829419</code></a>
Bump <code>@​types/semver</code> from 7.7.1 to 7.8.0 (<a
href="https://redirect.github.com/actions/setup-node/issues/1616">#1616</a>)</li>
<li><a
href="680d1e489b"><code>680d1e4</code></a>
Support absolute paths in node-version-file (<a
href="https://redirect.github.com/actions/setup-node/issues/1633">#1633</a>)</li>
<li><a
href="db0fb3a98e"><code>db0fb3a</code></a>
Bump baseline-browser-mapping from 2.10.38 to 2.11.22 (<a
href="https://redirect.github.com/actions/setup-node/issues/1629">#1629</a>)</li>
<li><a
href="94196ee1d1"><code>94196ee</code></a>
Bump pnpm/action-setup from 6.0.9 to 6.0.10 (<a
href="https://redirect.github.com/actions/setup-node/issues/1602">#1602</a>)</li>
<li>Additional commits viewable in <a
href="8207627860...949feb2413">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=7.0.0&new-version=7.1.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 18:13:43 +02:00
Ben Brandt
21eef57655 feat: replay each subagent under its live session id for subagents RFD clients (#1328)
A replay now restores subagents for a client on the [subagents
RFD](https://github.com/agentclientprotocol/agent-client-protocol/blob/main/docs/rfds/subagents.mdx)
form: a v1 client that declares `subagents` and isn't AIR, and every v2
client. Until now, its replay left children out. **AIR's replay is
unchanged.**

## The rules

The RFD asks that replayed children keep their original session ids and
known parentage, and that announcements come before child traffic. It
forbids guessing a parent or inventing an id, and asks that known gaps
be made apparent. It also requires all selected history before the
response.

- **Ids:** each child replays under the session id it had live, the SDK
agent id. Claude Code's `subagents/agent-<id>.meta.json` names the Agent
or Task call that launched agent `<id>`.
- **Order:**
- The child is announced on the session that made the launch, right
after the launch, with the same labels as live: the `name` and the role
as `title`, and the short `description`.
  - Its own transcript then goes to the child session.
  - A nested child is announced on the child that launched it.
- **Incoming messages:** the child's prompt is a `session_message` with
the launch's id and the parent as sender, as live. A message that a
SendMessage call delivered (`origin.kind: "coordinator"`) is matched to
its call by the call's text. Claude Code frames it with a preface and an
instruction, so the match checks that the delivered text contains the
call's message. It then has the SendMessage's id, text, and sender, as
live; without a match, its transcript id and the delivered text.
- **Harness text:** other user text in a child's transcript, such as
"[Request interrupted by user for tool use]", isn't shown, as the live
child session doesn't show it. Tool results stay.
- **Ends:** every child's end is an `idle` before the response. The end
comes from the first of these that applies:
  1. its last persisted task notification (its task id is the agent id)
  2. `cancelled`, if `meta.json` says the user stopped it
3. its foreground launch's result (a background launch's result is only
its launch)
  4. otherwise, `idle` with no stop reason
- **Gaps:**
- A launch without metadata has no known id, so it isn't announced; its
Agent call stays an ordinary tool call of the parent. A child whose
transcript can't be read replays only its association, as the RFD
allows.
- Either case adds one notice for a client that takes notices: "Some
subagent history is not available".
- A subagent message that the session transcript stores inline is left
out, since the child's history comes from its own transcript. Current
Claude Code doesn't store such messages.
- **v2:** a replayed child's messages are cleared before their first
replayed chunk, as the parent's already are. A v2 client may still hold
them.

## Changes in the code

- **`src/subagent-replay.ts` (new):** `RfdSubagentReplay` holds the
replay's children, deliveries, and ends. The replay loop in
`acp-agent.ts` calls it at the launch, for each child message, and at
the end.
- **`subagentHistory`:** returns each launch's whole metadata record
(name, role, description, background, stopped by the user). AIR and the
RFD form share `readSubagentMessages`.
- **The v2 bridge:** registers a child that a replay announces as
replaying too.

## Evidence

- **Live, v1:** I loaded a real Claude Code session from a live run: a
foreground `scout` resumed once by SendMessage, and a background
`sleeper` that a cancel stopped.
  - Both children replay under their live agent ids, with their labels.
- `scout`'s prompt and its SendMessage delivery have exactly the ids and
senders the live run used (`toolu_…HvPNZm` and `toolu_…cB3K9t`, from the
root).
- `scout` ends `end_turn`, and `sleeper` ends `cancelled`, from
`stoppedByUser`.
- **Live, v2:** the same session over v2 with replay. There were no v2
schema violations, each child's state is mirrored on its own stream, and
all of it comes before the resume response.
- **Tests:**
- v1: a full replay order with a nested child, a matched delivery,
dropped harness text, the ends, and the notice for a launch without
metadata; ends for a stopped child and for one with no known end; inline
subagent messages left out.
  - v2: a child's replayed message is cleared on the child session.
- **Mutation checks:** removing any of the rules fails a test:
  - the v2 child registration
  - the RFD replay
  - the delivery match
  - the inline skip
  - `stoppedByUser`
  - ignoring a background launch's result
  - dropping harness text
  - the notice
- **Golden files:** only the v2 `session-load-replay` changes. It gains
the notice, because its Task launch has no metadata. The AIR golden
files are unchanged.
- **Checks:** `npm run check`, build and all 2886 tests pass.

## Not in this PR

- **A SendMessage after a reload:** a SendMessage to an agent from
before a reload still finds no known child in the live runtime, so the
agent stays unexposed. A follow-up registers the replayed children, and
the children of a resume without replay, in the runtime.

## Release

Titled `feat:`, since a v1 client on the RFD form gets new replay
output. Retitle it if you'd rather not release it.
2026-10-13 18:13:21 +02:00
Ben Brandt
53df1c7b22 fix: end the tool calls of a cancelled turn before it reports the cancel (#1327)
## Problem

After `session/cancel`, a tool call that was still running never gets a
final status. On `main` (Claude Code 2.1.293, a v1 client that declares
no capabilities) all three of these stay `pending` for good:

- the root's own Bash call, cancelled while its command runs;
- a foreground Agent call;
- the Bash call of that Agent's subagent, which this client sees as a
root tool call with `_meta.claudeCode.parentToolUseId`.

v2 has the same problem, and so does the subagents RFD form: the
subagent's call stays pending in the child's session after the child
reports `idle cancelled`. Found in the live run of #1257.

## Cause

I recorded the raw SDK stream (`emitRawSDKMessages`) and the session
transcripts of a live run on `main`. There are two gaps.

1. **The window drops Claude Code's interrupted result.** For the root
Bash and the Agent call, Claude Code does send a result, inside the
post-cancel window of #1309 and #1313, before the turn's `result`: a
`tool_result` with `is_error: true`, stamped `non_execution_kind:
"user-rejected"`, then `[Request interrupted by user for tool use]`,
then the aborted result. The `if (session.cancelled) break;` in the
`user`/`assistant` case drops it.
2. **The subagent's call gets no result at all.** Claude Code writes its
interrupted result only to the subagent's own transcript
(`<session>/subagents/agent-*.jsonl`). The stream never carries it,
before or after the response, so no change to what the window delivers
can reach it.

Nothing else closes these calls. `closeUnfinished` runs only at
`end_turn`, over the turn's `foregroundToolCallIds`, and that set holds
none of the three: `task_started` removes a Bash or Agent call from it
(`registerLiveTask`), and a subagent's call is never in it.

## Change

Two rules, in the stream consumer:

- **Deliver the real result.** Until the client is told that the stopped
work ended (the prompt response on v1, `idle` on v2), the window
delivers the `tool_result` of a tool call that the client holds open,
with Claude Code's own text and `nonExecutionKind`. Only the tool call
is updated. The other effects of a tool result (async tasks, plan state,
the ExitPlanMode flow) stay dropped, and so does everything after that
report, which is what #1313 closed.
- **Close what has no result.** When a turn settles `cancelled` because
of `session/cancel`, the adapter closes every tool call still open with
`AcpToolCallRenderer.unfinished`, before it resolves the turn. That
covers the subagent's call, a `tool_use` that Claude abandoned before it
ran, and every call when the stream gives no results (the force-cancel
backstop). Both protocols end the work at `turn.resolve`, so one rule
serves v1 and v2.

Supporting pieces:

- The closing report names the subagent that made the call
(`Session.toolCallParents`, bounded like `dispatchedToolCalls`), so the
subagent runtime routes it with the call's other updates: to the child
session in the RFD form (an idle child still takes updates), to no child
that AIR ended, to the root for a client without subagents.
- A **background** subagent that the SDK has not reported ended keeps
its calls open. The RFD form lets a child run on through a cancel
(`NativeSubagentRuntime.parentCancelled`). Two tests merged with #1320
failed against my first version, which closed those calls, and that is
why the registry exists. A foreground subagent ends with the work that
waits on it.
- For a v2 client, an error result in the window is `cancelled`,
whichever kind Claude Code stamps. CLI 2.1.293 stamps a command that the
interrupt killed `user-rejected`, which `isAbortedToolResult` reads as a
refusal (`failed`). A tool that finished as the cancel landed stays
`completed`. A v1 client gets `failed`, as before, since v1 has no
`cancelled`.
- `unfinished` gets a `cancelled` variant: "The turn was cancelled
before this tool call returned a result."
- The rule applies to `session/cancel` only (`session.cancelled`), not
to the ExitPlanMode "keep planning" outcome, which also ends a turn
`cancelled`.
- README: one sentence on what an RFD client gets at a cancel.

## Why both rules

| | root Bash | Agent call | subagent's Bash |
| --- | --- | --- | --- |
| Deliver the real result only | fixed | fixed | **still pending** (no
result in the stream) |
| Close every open call only | fixed, made up | fixed, made up | fixed,
made up |
| Both (this PR) | Claude Code's result | Claude Code's result | made up
|

Closing alone would fix all three, but it throws away a result that
Claude Code already sent: its text, and the `nonExecutionKind` that lets
a client tell a rejection from an interruption. So the real result goes
out where one exists, and the adapter makes up a status only for what
Claude Code never reports. The Agent call ends first, then its
subagent's call, in the same turn and before the response.

## What does not change

- Output after the report stays dropped. A test pins that the tool of a
prompt the cancel abandoned, which a permission request announces after
the response, gets no result (it fails with the `workEndReported` check
removed).
- A result for a call the client does not hold open sends nothing and
logs nothing.
- AIR ends every child at the cancel, and the runtime drops every later
update to an ended child. The closing reports go through the same
routing and are dropped there, so nothing reaches an ended child (the
live AIR run logs one `ignoring late update for terminal subagent`).
- No golden file changes. None has a cancel, and the diff touches none.

## Evidence

**Live** (Claude Code 2.1.293, Sonnet, each tool call's statuses before
and after the prompt response; the probe allows each permission request
after 1.5 s and cancels 4 s later, with `python3 -c "import time;
time.sleep(30)"` so the command runs; every update is checked against
the SDK's v1 schema):

| | root Bash | Agent call | subagent's Bash | after the response |
| --- | --- | --- | --- | --- |
| `main`, plain v1 | `pending` | `pending` | `pending` | 0 updates |
| this PR, plain v1 | `failed`, Claude Code's text | `failed`, Claude
Code's text | `failed`, closed here | 0 updates, 0 schema violations |
| this PR, RFD client | `failed` | `failed` | `failed`, in the child's
session after its `idle cancelled` | 0 updates, 0 violations |
| this PR, AIR | `failed` | hidden, as before | none: the child ended,
and the runtime dropped the closing update | 0 updates |
| this PR, v2 | `cancelled` | `cancelled` | `cancelled`, in the child's
session, before the root's `idle cancelled` | 0 updates |

The plain and v2 runs are on this commit. The RFD and AIR runs were on
this change before it was rebased onto #1324 and #1325, which change the
v2 subagent surface and the routing of a subagent's hook update, and not
the cancel path. In the raw stream, the subagent's Bash has
`task_notification stopped` and no result. Its transcript has the
`is_error` result.

**Tests** (16 new, one rewritten, one extended), scripted in the order
recorded live:

- `incomplete-tools.test.ts`, plain client: the real result of a running
tool; an Agent call and its subagent's call; the force-cancel backstop;
a start that streams in after the cancel; a result after the response; a
stray result; an abandoned prompt's tool; a background subagent's call
left open; a call that a permission request announced and a later update
attributed; the registry's cleanup.
- `acp-agent.test.ts`: the RFD form (the stopped child's call closed in
its session, the running child's left open, the Agent call's result) and
AIR (nothing to an ended child, including a permission-announced call).
- `acp-v2.test.ts`: `cancelled` for the Agent call and its subagent's
call before `idle cancelled`, `completed` for a tool that finished, and
a cycle that Claude Code started.
- `preserves cancellation and does not attribute its unfinished tool to
the next turn` asserted that the cancelled turn's tool gets no `failed`
update, which is the behavior this PR changes. It now asserts one
report, before the cancelled response and not attributed to the next
turn.

**Mutation checks.** Each of these fails at least one test: no delivery
of the real result (8), no closing at the settle (10), delivery after
the report (1), delivery for calls the client does not hold open (1),
closing a running background subagent's calls (3), no parent on the
report (3), v2 real result not `cancelled` (2), `unfinished` not
`cancelled` on v2 (1), the closing text (4), `ran` ignored (2), the
registry kept after a call ends (1, added after the first run showed
that nothing caught it), the parent recorded only from a `tool_call`
(1), the v1 end-of-work signal on v2 (3), AIR's drop of updates to an
ended child removed (5).

`npm run check`, `npm run build` and `npm run test:run` (2898 tests)
pass.

## Related, and not covered

- #1221 closes the turn's `foregroundToolCallIds` at a cancelled settle
with a made-up `failed`. It overlaps with this change in `settleActive`
and in the test above, so the two will conflict. As far as I can tell
from `registerLiveTask`, it would not have closed the three calls here.
Its scenario, a start that streams in after the cancel, is covered by a
test here. #1221 also hooks the inline settle of a held turn in
`cancelTurns`, which this PR does not touch.
- Not covered: a background subagent's calls when a cancel stops it
while no turn settles (a cancel while idle, or of a held turn that
`cancelTurns` settles inline); the calls of a v2 cycle that Claude Code
started get Claude Code's results, but nothing closes the ones it sends
none for.
- The closing report carries no terminal exit, so a client that shows
command output sees the command's call end without one.
`closeUnfinished` does the same at `end_turn`.
- A background subagent whose end the SDK reports only after the turn's
result keeps its calls open. Claude Code 2.1.293 reports it before.
2026-10-13 18:10:14 +02:00
Ben Brandt
c7393d27ff refactor: experimental ACP v2 support for subagents (#1325)
Adds subagents to the experimental [ACP
v2](https://agentclientprotocol.com/protocol/v2/overview) surface. With
them, it covers v2's baseline session model, so this is the `feat:` that
announces v2 support. The README gains a section on how to enable it and
what it serves.

Based on #1324, a v1 fix that this PR's golden scenarios exposed. Merge
#1324 first; this PR then retargets to `main`.

## Experimental ACP v2

Started with `CLAUDE_AGENT_ACP_EXPERIMENTAL_V2=1`, the adapter serves
the draft ACP v2 to a client that asks for protocol version 2 or later
in `initialize`, and v1 to every other client, as before.

- **Served on v2:**
- the session baseline (`session/new`, `list`, `resume` with replay,
`close`, `prompt`, `cancel`)
  - `session/delete`, `session/fork`, and additional directories
  - config options (the mode is one), plans, notices, compaction
  - display terminals for commands
  - permission requests, elicitation, providers, auth
  - and now subagents
- **v1 only for now:** steering and the AIR extensions.

The draft can still change, and the surface changes with it.

## What changes

The [subagents
RFD](https://github.com/agentclientprotocol/agent-client-protocol/blob/main/docs/rfds/subagents.mdx)
makes subagent updates part of v2's baseline: "support is assumed rather
than negotiated".

- **Every v2 client gets subagents.** The v2 bridge declares `subagents`
in the v1 request it makes for a v2 client (`v1InitializeRequest`), so
the agent reports each Agent or Task subagent in the RFD form from
#1320–#1322.
- **The updates pass through:** `subagent_update`, `session_message` and
`session_message_chunk` have the same shape in v1 and v2.
- **The child's state goes to its own stream too.** v2 reports a child's
work state on the child's own stream, mirrored on the parent. So a
`subagent_update` with a `state` also becomes the child's
`state_update`, sent right after the parent's copy
(`v2ChildStateUpdate`). The announcement thus comes first, and both
copies of a state come before any later one, as the RFD requires. This
covers `requires_action`, and a failed child's `idle`/`error` with its
JSON-RPC `error`.
- **A state the RFD doesn't define throws,** as an update kind the
surface doesn't translate already does. The SDK types the open members
of v1's and v2's `StateUpdate` apart, so the bridge checks each state
against the RFD's before passing it on.
- **Replay doesn't flatten a child's inline text onto the root.** On
replay, a subagent's text goes to the root only for a client that gets
it there live: one without native subagent sessions that asked for the
flattened transcript. Before, an RFD client (v1, and now v2) got the
inline subagent text of a transcript replayed on the root, though the
RFD form doesn't replay children yet. Current Claude Code keeps subagent
messages in `subagents/`, not inline, so only the `session-load-replay`
scenario's inline message showed it. AIR's replay is unchanged.

## Live run

Against real Claude Code (Sonnet), with a v2 client that checks every
`session/update` and permission request against the SDK's v2 schema,
there were no schema violations:

- **Launch:**
- the child is announced on the root, then reports its own `running`,
then gets its prompt as a `session_message` from the root
- its tool calls arrive on its session, each with a title on its first
report
  - the Agent call's 35-second heartbeat stays on the root
- **Permission:**
- `requires_action` appears on the child's stream and on the parent's
copy, and on the root, whose turn waits on the child
  - the request goes to the child's session
  - then the child is back to `running`
- **SendMessage resume:**
  - the same child session runs again, with the root as sender
- the root turn goes idle while the resumed child keeps running, and the
child reports `idle`/`end_turn` later, which v2 allows for background
work
- **Cancel:** the child is `idle`/`cancelled` on both streams, then the
root is `idle`/`cancelled`.
- A tool call still running at the cancel never gets a final status.
That's a known bug on `main` for every client, fixed in a separate PR.

## Tests

- **End to end on v2:** an Agent call whose child fails with an SDK
error. Traffic arrives in order on both streams, and the child's `idle`
carries `error: { code: -32603, message }`.
- **Unit:**
  - the pass-through of the three updates
- the child's `state_update`, and no copy for an association update
without a state
  - the throw for a state and a stop reason the RFD doesn't define
- **v2 golden files:**
- the six subagent scenarios now show the RFD form: announcements,
prompts, child sessions, both copies of each state, and permission
requests on the child session
  - `session-load-replay` is unchanged from `main`
- the scenarios also check every message against the v2 schema, and that
each tool call's first report has a title (that check found #1324)
- **Mutation checks:** each of these fails tests:
  - not declaring `subagents`
  - dropping the child's copy
  - sending it before the parent's
  - dropping the state check
  - dropping the replay rule
- **v1 golden files:** unchanged.
- **Checks:** `npm run check`, build and all 2882 tests pass. CI runs
only on PRs to `main`, so I ran `ci.yml` on this branch by hand.

## Release

Titled `feat:`, so its title becomes the release note that announces v2.
2026-10-13 16:57:14 +02:00
Ben Brandt
4615a9141c fix: route a subagent's PostToolUse hook update to its child session (#1324)
## Problem

A PostToolUse hook update skipped the subagent router, for every client
with native subagent sessions (AIR's draft and the subagents RFD form):

- **A child's Edit or Write:** its final diff went to the **root**
session, with `parentToolUseId` set. All its other reports go to the
child session, so the client got an update for a tool call it doesn't
have on the root, and the child's tool call never got its final diff.
The RFD forbids moving a tool call between sessions.
- **AIR's hidden Agent/Task call:** AIR's draft hides the Agent/Task
tool call, but its hook update reached AIR anyway, for a tool call AIR
never received.

AIR's golden files had recorded both.

## Cause

`toAcpNotifications` registers a tool's PostToolUse hook the first time
it sees the `tool_use`, with the client passed to it. Claude Code
streams, so that first time is a stream event, and the stream path
passed `this.client` instead of the routed client that every other path
uses. The hook's update then went straight to the client.

## Change

The stream path passes the routed client, so a hook update goes through
`sendUpdate` and the subagent router like every other update. That
client is the only one the stream path uses.

Clients without native subagent sessions are unchanged: the router
passes their updates through as they are.

## Evidence

- **New test:** a child's Edit that is streamed, completed, and then
gets its PostToolUse hook. All the Edit's reports, including the hook's
diff, go to the child session. Without the fix, the hook's report goes
to the root (`Set{ 'agent-42', 'test-session' }`).
- **AIR golden files**, both intended:
- `subagent-task-legacy`: the child Edit's hook update (`locations`)
moves from `<session>` to `agent_1`, and the hidden Task call's hook
update is gone.
- `subagent-agent-async`: the hidden Agent call's hook update is gone.
In both cases it was the only report AIR got for that tool call.
- **How it was found:** declaring subagents for ACP v2 clients (the next
PR) runs the RFD form through the v2 golden scenarios, whose check that
a tool call's first report has a title caught the root-session diff.
- **Checks:** `npm run check`, build and all 2878 tests pass.
2026-10-13 16:54:34 +02:00
Ben Brandt
252126f616 feat: report a subagent as requires_action while its request is open for subagents RFD clients (#1322)
The third of three stacked PRs that replace #1257. It's based on #1321,
so its diff shows only the `requires_action` part. Merge #1320 and #1321
first; this PR then retargets to `main`.

1. #1320: the RFD form for a v1 client that declares `subagents` and
isn't AIR.
2. #1321: a child keeps its session across SendMessage resumes.
3. **This PR:** `requires_action` while a child's request is open.

**AIR is unchanged.**

## What changes

The [subagents
RFD](https://github.com/agentclientprotocol/agent-client-protocol/blob/main/docs/rfds/subagents.mdx)
reports a child that waits for the user as `requires_action`.

- **While a request is open:** a child whose permission or question
request is open reports `requires_action` on its parent, then `running`
again when its last open request ends
(`NativeSubagentRuntime.awaitingUser`). The agent wraps the permission
and elicitation requests of a child session in it
(`withChildAwaitingUser`).
- **Overlapping requests** keep `requires_action` until the last one
ends. A child that ended while it waited doesn't report `running` again.
- **A resume while a request is open** reports `requires_action`, not
`running`.

## Live run

Against real Claude Code (Sonnet), a child's Bash permission request
showed `running` → `requires_action` while the request was open →
`running` once it was answered. No schema violations.

## Follow-ups (low severity, not in this PR)

- **Brief `running` after a withdrawal:** after a cancel withdraws a
child's request, the child briefly reports `running` before the SDK's
stop event. The same happens at a stream end, whose cleanup aborts every
pending request.
- **Ignored cancel requests:** AskUserQuestion's `createElicitation` is
not raced against the abort. If a client ignores `$/cancel_request`, the
child's open-request count stays up, and a later resume reports
`requires_action`.

## Tests

- **End to end:** `requires_action` around a child's permission request.
The cancel test now also expects the background child's
`requires_action` before the cancel.
- **Runtime:** overlapping requests, a child that ended while it waited,
and a resume with an open request.
- **Checks:** `npm run check`, build and all 2877 tests pass on this
branch, whose tree is #1257's with `main` merged in. CI runs only on PRs
to `main`, so I ran `ci.yml` on this branch by hand.
2026-10-13 16:11:25 +02:00
Ben Brandt
bd06804b67 feat: keep a subagent's session across SendMessage resumes for subagents RFD clients (#1321)
The second of three stacked PRs that replace #1257. It's based on #1320,
so its diff shows only the resume part. Merge #1320 first; this PR then
retargets to `main`.

1. #1320: the RFD form for a v1 client that declares `subagents` and
isn't AIR.
2. **This PR:** a child keeps its session across SendMessage resumes.
3. #1322: `requires_action` while a child's permission or question
request is open.

**AIR is unchanged:** it keeps a new session per generation, as in its
draft.

## What changes

The [subagents
RFD](https://github.com/agentclientprotocol/agent-client-protocol/blob/main/docs/rfds/subagents.mdx)
gives a child one session id across every delegation to it. With #1320
alone, a resumed child stays `idle`.

| RFD | With #1320 | Now |
| --- | --- | --- |
| One session id per child, across delegations | a resumed child stays
`idle`, and its resumed work isn't shown | the child runs again in the
same session, under the SDK agent id. The CLI runs a resumed agent under
the SendMessage call's tool id, so that call becomes the child's current
delegation: the resumed run's output and end reach the child |
| The parent's message to the child is an incoming `session_message` |
only the launch prompt | each SendMessage that resumes the child is a
`session_message` with the SendMessage tool use's id. Its sender is the
session that made that call, or left out when unknown. A repeated resume
signal doesn't repeat it |

Out-of-order events, from the reviews of #1257:
- **Late end:** the end of an earlier delegation ends nothing
(`isStaleEnd`). It also no longer settles the resumed run's live task at
the agent level.
- **Late result:** a SendMessage result that arrives after its run ended
resumes nothing (`isEndedDelegation`).
- **Early output:** a resume's output that arrives before its
`task_started` is buffered, then delivered to the child.
- **Reused id:** a run under an id the child used before ends the run,
since the SDK falls back to it.
- **Running patch:** a `task_updated` running patch reports `running`
without its guessed prompt or call.

## Live run

Against real Claude Code (Sonnet), a SendMessage to a finished child ran
it again in the same session. Its message arrived with the SendMessage's
id and the root as sender, and the turn waited for the resumed run. No
schema violations.

## Tests

- **End to end:**
- a SendMessage resume under the SendMessage's tool id: one session, two
prompts, the resumed output in the child, a stale end ignored
- the resumed run's live task kept when an earlier delegation's end
arrives late
  - a late SendMessage result
- **Runtime:**
  - reuse and resume
  - a resume's output buffered before its `task_started`
  - the sender of a resume's prompt: the root, a child, or unknown
  - a run under a reused id
  - a running patch
  - a result without a text
- **Mutation checks during #1257's reviews:** removing any of these
rules fails tests.
- **Checks:** `npm run check`, build and all 2873 tests pass on this
branch. CI runs only on PRs to `main`, so I ran `ci.yml` on this branch
by hand.
2026-10-13 16:11:25 +02:00
Ben Brandt
5758ac84a4 feat: report subagents as the subagents RFD defines, for clients that are not AIR (#1320)
The first of three stacked PRs that replace #1257, split for review.
Together they bring v1 clients the [subagents
RFD](https://github.com/agentclientprotocol/agent-client-protocol/blob/main/docs/rfds/subagents.mdx),
as revised by agentclientprotocol/agent-client-protocol#2281 and #2308.
**AIR is unchanged**: its golden files don't change.

1. **This PR:** the RFD form for a v1 client that declares `subagents`
and isn't AIR.
2. #1321: a child keeps its session across SendMessage resumes.
3. #1322: a child reports `requires_action` while its permission or
question request is open.

Merge them in that order. Each later PR is based on the previous branch,
so its diff shows only its own part.

## Who gets what

- **AIR** (any client that declares `_meta.jetbrains.air`): the earlier
draft it implements, exactly as before. That's `subagent_spawned`,
`subagent_state_update`, a session per generation, the hidden Agent/Task
tool call, and draft replay. AIR keeps it whichever signal it declares.
- **A v1 client with `clientCapabilities.subagents` that isn't AIR:**
the RFD form below. SDK 1.8.0 types it (`subagent_update`,
`session_message`, the v1 `StateUpdate`).
- **Without either signal:** nothing changes.

`subagentForm` (`src/acp-subagents.ts`) picks the form.
`NativeSubagentRuntime` keeps the routing, buffering and nested lineage,
and renders either form.

## The RFD form

| RFD | Before | Now |
| --- | --- | --- |
| Announce the child before any of its traffic | `subagent_spawned {
name, task, prompt }` | `subagent_update { sessionId, title,
description, state: { state: "running" } }` |
| `description` is the role or purpose, "not a transcript of
instructions" | `task` was the prompt | `title`: the agent's `name`,
with its role (`subagent_type`) after it, else the role. `description`:
the Agent input's short `description` |
| The parent's message to the child is an incoming `session_message` |
`prompt` on the announcement | a `session_message` in the child's
transcript, with the Agent/Task tool use's id and the session that made
the call as sender |
| Delegation tools stay tool calls | the Agent/Task tool call was hidden
| it stays a tool call of the session that made it, with the report its
model got. Its heartbeats and result stay there too |
| Work-state snapshots, not a terminal lifecycle | `completed`,
`failed`, `cancelled`, `disconnected` | `running`; `idle` with
`end_turn`, `cancelled`, or `error` with the SDK's error text as its
JSON-RPC `error`; `idle` with no stop reason for work that ended without
a known reason (a worker restart, or still running when the stream ends)
|
| `idle` isn't terminal | later updates were dropped | an idle child
still gets its updates, such as its background work's |
| A cancel stops the work it waits on; other descendants are up to the
Agent (#2308) | `session/cancel` ended every child as cancelled | the
adapter ends no child itself. Claude Code's interrupt stops the
foreground children, and currently background ones too; each reports
`idle`/`cancelled` from the SDK's events, and the foreground ones'
requests are withdrawn |
| Never guess a parent | a child that ended before its spawning frame
was announced under the root | it stays unexposed. A nested child waits
for its parent's announcement |
| Never move a tool call | | a tool call that a permission request
emitted on the root before its child was announced stays on the root
after a cancel. AIR keeps the cancel's cleanup, since its draft ends
every child there |
| No agent-side capability | `sessionCapabilities.subagents` for every
client | for AIR only |

## Not in this PR

- **SendMessage resumes** (#1321). Until then a resumed child stays
`idle`. Requests find a child by its agent id, so only the permission
and question requests of the resumed run appear in its session. A resume
never opens a second session for the child, which the RFD forbids, and a
test pins that.
- **`requires_action`** (#1322).
- **Replay** doesn't expose children yet. Today's replay invents
`<session>:replay-subagent:<id>` ids, and the RFD requires the original
ones. The RFD makes child replay a SHOULD, so an RFD client's replay
leaves children out until the replay PR.

## Settled during the review of #1257

- **Cancel cascade:** the RFD leaves background children to the Agent
(#2308). An agent-level test pins #2308's validation scenario.
- **Session ids across forks:** the raw SDK agent id stays the session
id. The SDK's fork copies no subagent history, so a fork can't repeat an
id under another root. The reasoning is on `nextChildSessionId`.
- **Held turns:** the adapter holds a v1 prompt open while a background
subagent the turn started runs. A cancel still settles the prompt.

## Live run

Against real Claude Code (Sonnet), with a v1 client that declares
`subagents` and checks every update and permission request against the
SDK's v1 schema, there were no schema violations:

- **Launch:** `running` with the title (`scout (general-purpose)`) and
description, the prompt as a `session_message` from the root, and the
child's tool calls in its session. Then `idle`/`end_turn` before the
Agent call completes on the root.
- **A 40-second child:** the Agent call's heartbeat and result stay on
the root. Before the fix, they moved to the child, because the heartbeat
named the Agent call as its own parent.
- **Cancel:** a foreground child is `idle`/`cancelled` 15 ms after
`session/cancel`, and the prompt returns `cancelled`. Claude Code
stopped a background child too (its metadata says `stoppedByUser`).
- **Found on `main`, for every client, and fixed separately:** after a
cancel, a tool call that was running never gets a final status.

## Tests

- **AIR:** the end-to-end tests of the earlier draft run with an AIR
client that declares `nativeSubagentSessions` (`AIR_NATIVE_SUBAGENTS`).
The AIR golden files are unchanged.
- **RFD, end to end:**
  - announcement and prompt
- a failure with the SDK's error, also when the error arrives after the
failure
  - a stop, and a worker-restart orphan
  - no draft updates
  - the cancel per #2308
- the raced tool call after a cancel, kept on the root, with AIR's
cleanup kept
  - the Agent call's heartbeat

  Every update is checked against the SDK's v1 schema.
- **RFD, runtime:**
  - the visible control tool call, also nested
  - labels, also from a late frame
- an unparented child staying unexposed, and a nested child announced
only after its parent
  - nothing reported at the parent's cancel
  - the end of the stream, and `unknown`
  - a resume opening no second session
- **Plain/Zed comparison with origin/main:** it now allows the missing
agent capability, which `compare.ts` documents.
- **Mutation checks during #1257's reviews:** removing any of these
rules fails tests.
- **Checks:** `npm run check`, build and all 2863 tests pass.

## Release

Titled `feat:`, because it changes what a v1 client that declares
`subagents` receives.
2026-10-13 16:11:24 +02:00
Sergey Shkredov
d6c371704f fix: display active Central helpers as JetBrains Air Gateway (#1266)
### Summary

Addresses [IJAI-2956](https://youtrack.jetbrains.com/issue/IJAI-2956).

- Recognize Central only when the active key source is `apiKeyHelper`
and its effective helper command contains the whole word `central` and
the word sequence `proxy start`, case-insensitively, allowing multiple
whitespace characters.
- Read the effective command through the existing `SettingsManager`/SDK
settings resolution, including project/managed overrides, rather than
reimplementing configuration merging or executing helpers.
- Return `kind: gateway` and the exact label `JetBrains Air Gateway`,
without `detail` or invented account identity. URL, flags, routes, and
endpoint ownership are intentionally not checked: this is an agreed weak
display heuristic, not proof of provenance or entitlement.
- Render ordinary helpers as `API key helper` instead of raw
`apiKeyHelper`; do not expose unknown key-source identifiers. Preserve
external backend and client-owned configuration priority, CLI/SDK
merging, stale-probe protection, and duplicate suppression.
- Add mapper and ACP-agent regression coverage for positive/negative
commands, overrides, CLI/SDK results, and source changes; update
`docs/air-extensions.md`.

Examples matching the heuristic include `central proxy start` and
`/opt/bin/CeNtRaL PrOxY StArT --arbitrary-flag`. `decentralized proxy
start`, `central start`, and an inactive helper do not match.

### Sanitized auth status

```json
{
  "kind": "gateway",
  "label": "JetBrains Air Gateway"
}
```

No helper command, key, header, private route, or invented account
information is sent.

### Verification

Based on fresh upstream `main` at `0724b17`, rechecked immediately
before publication. Changes and rebuilt `dist` are available in the main
local clone; the user confirmed the local adapters work.

```bash
npm run build
npm run lint
npm run test:run
```

- Build and lint passed.
- Full test suite: **2484 passed, 30 existing skipped** across 44 passed
test files and 4 skipped files.
- No new skip flags or disabled tests were used.

### Scope

No AIR UI, provisioning, personal configuration migration, effort/mode
changes, or new ACP protocol. Plan files and personal configurations are
not committed.

---------

Co-authored-by: Junie <junie@jetbrains.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Andrey Bragin <andrey.bragin@jetbrains.com>
2026-10-13 14:29:48 +02:00
176 changed files with 17823 additions and 1244 deletions

View File

@@ -24,9 +24,10 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Use Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1
with:
node-version: "lts/*"
check-latest: true
cache: "npm"
- name: Install dependencies

View File

@@ -74,9 +74,10 @@ jobs:
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref }}
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1
with:
node-version: "lts/*"
check-latest: true
registry-url: "https://registry.npmjs.org"
- run: npm ci
- run: npm run build
@@ -129,9 +130,10 @@ jobs:
# few megabytes packed, so a full fetch is cheap and sidesteps every
# shallow-clone tag caveat.
fetch-depth: 0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1
with:
node-version: "lts/*"
check-latest: true
registry-url: "https://registry.npmjs.org"
# Ahead of the version bump, so this still validates the committed lockfile.
- run: npm ci

View File

@@ -1,3 +1,3 @@
{
".": "0.89.1"
".": "0.94.0"
}

View File

@@ -1,5 +1,99 @@
# Changelog
## [0.94.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.93.0...v0.94.0) (2026-10-19)
### Features
* send one end event after the session/load history replay for AIR ([#1361](https://github.com/agentclientprotocol/claude-agent-acp/issues/1361)) ([1420994](https://github.com/agentclientprotocol/claude-agent-acp/commit/1420994ced20919aad127a8bd0294de022449e35))
### Bug Fixes
* match OpenRouter model ids in the gateway model listing ([#1365](https://github.com/agentclientprotocol/claude-agent-acp/issues/1365)) ([bcf74c2](https://github.com/agentclientprotocol/claude-agent-acp/commit/bcf74c2fe8e32bd24714d45122529dcf2f99a279))
* resolve a skill's SKILL.md by its declared name, not by its directory name ([#1372](https://github.com/agentclientprotocol/claude-agent-acp/issues/1372)) ([424db87](https://github.com/agentclientprotocol/claude-agent-acp/commit/424db8753581bfaf4c5bf278f8df8c089eec35da))
* update to ACP SDK 2.0.0 ([#1386](https://github.com/agentclientprotocol/claude-agent-acp/issues/1386)) ([ff01e06](https://github.com/agentclientprotocol/claude-agent-acp/commit/ff01e06579225e00643914692994cd60da4d6225))
* update to Claude Code 2.1.303 ([#1384](https://github.com/agentclientprotocol/claude-agent-acp/issues/1384)) ([2dab922](https://github.com/agentclientprotocol/claude-agent-acp/commit/2dab92264414d133fd6774f7a90241ea5192ba32))
## [0.93.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.92.0...v0.93.0) (2026-10-16)
### Features
* add /remote-control command to start Remote Control for a session ([#1197](https://github.com/agentclientprotocol/claude-agent-acp/issues/1197)) ([db9f50f](https://github.com/agentclientprotocol/claude-agent-acp/commit/db9f50f02145d088c0f6080672969a0673ba6359))
### Bug Fixes
* hold a fork back from the session list until its session/fork response is out ([#1368](https://github.com/agentclientprotocol/claude-agent-acp/issues/1368)) ([f5967fd](https://github.com/agentclientprotocol/claude-agent-acp/commit/f5967fd6b49ce4821cc829c26902ccf46f671992))
## [0.92.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.91.0...v0.92.0) (2026-10-16)
### Features
* add the MCP server and tool to AIR tool calls ([#1359](https://github.com/agentclientprotocol/claude-agent-acp/issues/1359)) ([872e93f](https://github.com/agentclientprotocol/claude-agent-acp/commit/872e93f11fde8abc528822d29cebcc542b05e9e8))
* publish the predicted follow-up prompt as _meta.promptSuggestions ([#1356](https://github.com/agentclientprotocol/claude-agent-acp/issues/1356)) ([78a29f5](https://github.com/agentclientprotocol/claude-agent-acp/commit/78a29f58ba1eb1b54896d8d73f49402ee50aa087))
* update ACP SDK to 1.9.0 and adopt the v2 spec clarifications ([#1366](https://github.com/agentclientprotocol/claude-agent-acp/issues/1366)) ([68bf74f](https://github.com/agentclientprotocol/claude-agent-acp/commit/68bf74f58ee378f21502ac93c44878b712905af2))
### Bug Fixes
* include subagent file edits in the file-change report ([#1357](https://github.com/agentclientprotocol/claude-agent-acp/issues/1357)) ([910d83c](https://github.com/agentclientprotocol/claude-agent-acp/commit/910d83ceea78a03b39002cf2dcc275442838e2db))
* restore resumed subagents after an adapter restart ([#1364](https://github.com/agentclientprotocol/claude-agent-acp/issues/1364)) ([26f144a](https://github.com/agentclientprotocol/claude-agent-acp/commit/26f144a2a5c60452805d11c87433963318ef4e65))
## [0.91.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.90.0...v0.91.0) (2026-10-15)
### Features
* keep background subagents running after a parent cancel, for clients that can cancel them ([#1348](https://github.com/agentclientprotocol/claude-agent-acp/issues/1348)) ([ba3c7be](https://github.com/agentclientprotocol/claude-agent-acp/commit/ba3c7be05e9af297fe95b0ac647da2340dd9e15b))
* let a client cancel one subagent with session/cancel on its child session ([#1347](https://github.com/agentclientprotocol/claude-agent-acp/issues/1347)) ([aeb69e7](https://github.com/agentclientprotocol/claude-agent-acp/commit/aeb69e7ed1170e8bce236378432ef8b83cdf2554))
* mark session list rows of worktrees Claude Code made (agentWorktree) ([#1352](https://github.com/agentclientprotocol/claude-agent-acp/issues/1352)) ([1a1f1a0](https://github.com/agentclientprotocol/claude-agent-acp/commit/1a1f1a0e802cc2ee097d9c78c1f7bafccef7c3df))
* narrow models to what the providers/set gateway lists ([#1340](https://github.com/agentclientprotocol/claude-agent-acp/issues/1340)) ([e8eb9d3](https://github.com/agentclientprotocol/claude-agent-acp/commit/e8eb9d340fd4f6dfd9826cf58461edf5ee165f44))
* restore a subagent from before a reload when SendMessage resumes it, for subagents RFD clients ([#1332](https://github.com/agentclientprotocol/claude-agent-acp/issues/1332)) ([df21b3f](https://github.com/agentclientprotocol/claude-agent-acp/commit/df21b3fb56c3449b6f86c9c9df558be351815ed3))
### Bug Fixes
* close the tool calls of a background subagent that the SDK reports stopped after a cancel ([#1336](https://github.com/agentclientprotocol/claude-agent-acp/issues/1336)) ([9a1b90f](https://github.com/agentclientprotocol/claude-agent-acp/commit/9a1b90fb9db8b04ef6756cbcdc8df22afc82a2ba))
* close the tool calls of a cycle that Claude Code started and a cancel stopped ([#1339](https://github.com/agentclientprotocol/claude-agent-acp/issues/1339)) ([1fd217f](https://github.com/agentclientprotocol/claude-agent-acp/commit/1fd217f3e77d0b7883ebd220bc19c7899d3af77f))
* close the tool calls that a cycle Claude Code started abandoned, at its end ([#1344](https://github.com/agentclientprotocol/claude-agent-acp/issues/1344)) ([594aa8c](https://github.com/agentclientprotocol/claude-agent-acp/commit/594aa8c9556609c16946c889e9c8f1f76337b203))
* deliver the output of a background subagent that a cancel did not stop ([#1345](https://github.com/agentclientprotocol/claude-agent-acp/issues/1345)) ([bcc024d](https://github.com/agentclientprotocol/claude-agent-acp/commit/bcc024d2c9a34d62e19999bc6cfc393ad56fbcde))
* end the terminal of a command whose tool call is closed without a result ([#1335](https://github.com/agentclientprotocol/claude-agent-acp/issues/1335)) ([764f339](https://github.com/agentclientprotocol/claude-agent-acp/commit/764f339c3e83eac8ca34f4ddc8ed65f8199b651d))
* end the tool calls of a cycle that Claude Code started and a cancel stopped, on v1 ([#1342](https://github.com/agentclientprotocol/claude-agent-acp/issues/1342)) ([b4788d6](https://github.com/agentclientprotocol/claude-agent-acp/commit/b4788d61eaf9372b376d56c0f6fb7c6156822ae6))
* keep a live session that was never prompted when a resume or load changes its settings ([#1353](https://github.com/agentclientprotocol/claude-agent-acp/issues/1353)) ([0c8de13](https://github.com/agentclientprotocol/claude-agent-acp/commit/0c8de1380fd990291fc51cf17c6742402efb5c94))
* let session/delete succeed for a session without a transcript ([#1351](https://github.com/agentclientprotocol/claude-agent-acp/issues/1351)) ([fadbe18](https://github.com/agentclientprotocol/claude-agent-acp/commit/fadbe1881cd441609a1fde8c43286d9a404dc425))
* report no running for the session's own request that a cancel withdraws, on ACP v2 ([#1346](https://github.com/agentclientprotocol/claude-agent-acp/issues/1346)) ([d528647](https://github.com/agentclientprotocol/claude-agent-acp/commit/d528647162ba6747654125cf6ebc2297ced02ed0))
* show nothing of a tool that cancelled work asks to run after the cancel was reported ([#1334](https://github.com/agentclientprotocol/claude-agent-acp/issues/1334)) ([ebf8b49](https://github.com/agentclientprotocol/claude-agent-acp/commit/ebf8b49fd68924f2c50680e7e73241f625801980))
* stop waiting for a question the client leaves open after Claude Code cancels it ([#1338](https://github.com/agentclientprotocol/claude-agent-acp/issues/1338)) ([c1e83b4](https://github.com/agentclientprotocol/claude-agent-acp/commit/c1e83b46b717230a5d623c3ac591d915a776e0ae))
* update Claude Agent SDK to 0.3.300 ([#1355](https://github.com/agentclientprotocol/claude-agent-acp/issues/1355)) ([761b4c8](https://github.com/agentclientprotocol/claude-agent-acp/commit/761b4c835985ff265d35338df35eee6e6861d8f3))
## [0.90.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.89.1...v0.90.0) (2026-10-13)
### Features
* accept IDE session hooks without dropping model settings ([#1265](https://github.com/agentclientprotocol/claude-agent-acp/issues/1265)) ([fe8b69e](https://github.com/agentclientprotocol/claude-agent-acp/commit/fe8b69edd438ee1bc4206752b029dd45e71975fe))
* **deps:** Bump actions/setup-node from 7.0.0 to 7.1.0 ([#1329](https://github.com/agentclientprotocol/claude-agent-acp/issues/1329)) ([c6510c2](https://github.com/agentclientprotocol/claude-agent-acp/commit/c6510c27c11bb59ad10090c8c18ba60c0fa1c3fa))
* keep a subagent's session across SendMessage resumes for subagents RFD clients ([#1321](https://github.com/agentclientprotocol/claude-agent-acp/issues/1321)) ([bd06804](https://github.com/agentclientprotocol/claude-agent-acp/commit/bd06804b67d7b4efa8180db2f36203a5d34c3ac4))
* replay each subagent under its live session id for subagents RFD clients ([#1328](https://github.com/agentclientprotocol/claude-agent-acp/issues/1328)) ([21eef57](https://github.com/agentclientprotocol/claude-agent-acp/commit/21eef576557c845bfe9acc66499dab347f813a98))
* report a subagent as requires_action while its request is open for subagents RFD clients ([#1322](https://github.com/agentclientprotocol/claude-agent-acp/issues/1322)) ([252126f](https://github.com/agentclientprotocol/claude-agent-acp/commit/252126f61654cf7ac2cad74be53b4c6add569e1f))
* report subagents as the subagents RFD defines, for clients that are not AIR ([#1320](https://github.com/agentclientprotocol/claude-agent-acp/issues/1320)) ([5758ac8](https://github.com/agentclientprotocol/claude-agent-acp/commit/5758ac84a4a3b95ace7542a7e71fd58d93a581b3))
### Bug Fixes
* close the open tool calls of a held or settling turn that a cancel ends ([#1221](https://github.com/agentclientprotocol/claude-agent-acp/issues/1221)) ([32c23db](https://github.com/agentclientprotocol/claude-agent-acp/commit/32c23db06d53d7d4e1295d78fc0129f8278d75df))
* display active Central helpers as JetBrains Air Gateway ([#1266](https://github.com/agentclientprotocol/claude-agent-acp/issues/1266)) ([d6c3717](https://github.com/agentclientprotocol/claude-agent-acp/commit/d6c371704fb5fd74c3b329a3d756486975e3f180))
* drop streamed output of prompts a cancel abandoned ([#1313](https://github.com/agentclientprotocol/claude-agent-acp/issues/1313)) ([1137df3](https://github.com/agentclientprotocol/claude-agent-acp/commit/1137df33aa4fe387916596ff489c02ee48ccbba3))
* end a cancelled turn at its interrupted result and deliver Claude Code's follow-up after a cancel ([#1309](https://github.com/agentclientprotocol/claude-agent-acp/issues/1309)) ([b007ac7](https://github.com/agentclientprotocol/claude-agent-acp/commit/b007ac7c45b85755877fee802c2c901a0e3057c6))
* end the tool calls of a cancelled turn before it reports the cancel ([#1327](https://github.com/agentclientprotocol/claude-agent-acp/issues/1327)) ([53df1c7](https://github.com/agentclientprotocol/claude-agent-acp/commit/53df1c7b22bf08f9a2866389be812be80f34c8a3))
* keep the terminal of a command in the background running until its task ends ([#1312](https://github.com/agentclientprotocol/claude-agent-acp/issues/1312)) ([d074558](https://github.com/agentclientprotocol/claude-agent-acp/commit/d074558e4a2ef4ee6f4b39aecb5ebf36d8c0f84b))
* report a command whose result says interrupted as cancelled on the experimental ACP v2 surface ([#1301](https://github.com/agentclientprotocol/claude-agent-acp/issues/1301)) ([b3bed2f](https://github.com/agentclientprotocol/claude-agent-acp/commit/b3bed2f7630f4ddb97978246255ba89ca6e4613e))
* route a subagent's PostToolUse hook update to its child session ([#1324](https://github.com/agentclientprotocol/claude-agent-acp/issues/1324)) ([4615a91](https://github.com/agentclientprotocol/claude-agent-acp/commit/4615a9141c12e8cd9093a5bbc0e62a7d778cf0cf))
* start a forked session so the client can prompt it at once ([#1317](https://github.com/agentclientprotocol/claude-agent-acp/issues/1317)) ([383bc7d](https://github.com/agentclientprotocol/claude-agent-acp/commit/383bc7dcf0d4d624d2d74405eb3cee6453eb9b11))
## [0.89.1](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.89.0...v0.89.1) (2026-10-10)

View File

@@ -42,22 +42,57 @@ Task subagent as its own child session, following the [subagents RFD](https://gi
- `subagent_update` on the parent announces the child before any of its traffic, with the
parent's `title` and `description` for it, and reports its work state: `running`,
`requires_action` while one of its requests is open, and `idle` when its work ends, with
`requires_action` while one of its requests is open (and `running` again when the last one ends,
unless a cancel withdrew it: the child is stopping then), and `idle` when its work ends, with
`end_turn` or `cancelled` when the SDK gives the reason. A failure is `idle` with the `error`
stop reason and the SDK's error text as its `error`. The child keeps its session for every
delegation to it.
- The prompt of the Agent or Task call that launches the child, and the message of each
SendMessage call that resumes it, is a `session_message` in the child's transcript. The tool
call stays a tool call of the session that made it.
- Session messages replace the Agent or Task call that launches the child, which its parent is
not shown:
- The launch's prompt is a `session_message` in both transcripts: the parent's outgoing view and
the child's incoming one. Both use the launch's tool use id, and each names the other with the
draft RFD's `recipientMessageId` and `senderMessageId`.
- A foreground child's answer, from the launch's result, is the child's `session_message` to
the parent. In Claude Code's hand-back mode, the child's `SubagentHandback` call is its
message to the parent instead, in both transcripts and linked the same way.
- A launch whose child is never exposed is shown as a tool call after all, with its final
status.
- The message of a SendMessage call that resumes the child is a `session_message` in the child's
transcript. The SendMessage call stays a tool call of the session that made it.
- `session/cancel` on the parent interrupts Claude Code, which stops the foreground children that
the turn waits on and, currently, background children too. Each stopped child reports `idle`
with `cancelled` from Claude Code's own events, and the open requests of the foreground ones are
withdrawn. The adapter ends no child at a cancel itself, so a child that keeps running keeps its
open requests. A child that still runs when Claude Code exits is `idle` with no stop reason.
the turn waits on. Background children keep running, as the RFD prefers; a client stops one with
its own `cancel`. Each stopped child reports `idle` with `cancelled` from Claude Code's own
events, and its open requests are withdrawn. The adapter ends no child at a cancel itself, so a
child that keeps running keeps its open requests and its open tool calls. To spare background
children, the adapter declares the SDK option `perTaskStopAffordance`, which a client can turn
off in `_meta.claudeCode.options`. That option spares background workflows too, which a client
cannot stop, so the adapter stops them after the interrupt, as Claude Code did without it.
Background commands keep running after a cancel either way. A tool call of a stopped child that Claude Code reports no
result for, such as the command it was running, ends in the child's session before the cancelled
prompt resolves. When Claude Code stops a background child after that, or while no prompt runs,
the tool call ends right after the child reports `idle`. A child that still runs when Claude Code
exits is `idle` with no stop reason.
- A child whose parent is not yet known when it ends is not exposed, since the RFD forbids
guessing its parent.
- A client cannot prompt, cancel, or otherwise change a child yet, and a replay does not restore
children yet.
- A replay restores each child under its live session id, from its own transcript, with the same
session messages as live, and its end is an `idle` before the response.
A launch without Claude Code's metadata file for the child stays an ordinary tool call of the
parent, and a notice, for a client that takes notices, says that subagent history is missing.
- After a load or resume, with or without replay, a SendMessage that resumes an agent from before
it runs that agent in its session from before. Before the agent's traffic, each of its ancestors
that the client was not told about in this connection is announced, parent before child and
with no work state, as the RFD requires. An agent whose launch the session's history does not
record stays unexposed.
- Each running child is announced with the `cancel` capability. `session/cancel` with the child's
session id stops that child's current work, through the SDK's `Query.stopTask`, and not its
parent's: Claude Code also stops the foreground children it waits on, and the parent's turn goes
on. Each stopped child reports `idle` with `cancelled` from Claude Code's own events, and its
requests that the stop withdraws report no `running` first. A replayed child, which no longer
runs, gets no capabilities. A client cannot prompt or otherwise change a child.
An [ACP v2](#experimental-acp-v2) client gets the same without declaring anything, since subagents
are part of v2's baseline. Each child also reports its work state as its own `state_update`, right
after the parent's copy.
JetBrains AIR keeps the earlier draft of the RFD (`subagent_spawned`, `subagent_state_update`),
which it enables with `nativeSubagentSessions` in `_meta.jetbrains.air.capabilities`. See
@@ -67,6 +102,22 @@ Without either signal, Agent and Task stay ordinary tool calls and child interac
root session. Clients that use the historical `_meta["subagent-transcript"]` capability or the
`forwardSubagentText` session option retain the flattened child transcript.
### Experimental ACP v2
The adapter also serves the [draft ACP v2](https://agentclientprotocol.com/protocol/v2/overview):
a client that asks for protocol version 2 or later in `initialize` gets v2, and every other client
gets v1. The draft can still change, and this surface changes with it.
`CLAUDE_AGENT_ACP_PROTOCOL` limits the adapter to one version. With `v1`, a client that asks
for v2 is negotiated down to v1. With `v2`, the adapter rejects the `initialize` of a v1 client.
`auto`, the default, serves both.
On v2 the adapter serves the session baseline (`session/new`, `session/list`, `session/resume`
with replay, `session/close`, `session/prompt`, `session/cancel`), `session/delete`,
`session/fork`, additional directories, config options (the mode is one), plans, notices,
compaction, display terminals for commands, permission requests, elicitation, providers, auth, and
[subagents](#subagent-sessions). Steering and the AIR extensions are v1 only for now.
## Contribution Policy
This project does not require a Contributor License Agreement (CLA). Instead, contributions are accepted under the following terms:

View File

@@ -20,14 +20,17 @@ It describes only this adapter.
- [Session hooks](#session-hooks)
- [Permission presentation](#permission-presentation)
- [Goal](#goal)
- [Predicted follow-up prompt](#predicted-follow-up-prompt)
- [Recommended config values](#recommended-config-values)
- [Async tasks](#async-tasks)
- [Agent file-change report](#agent-file-change-report)
- [Session failure](#session-failure)
- [Native subagent sessions](#native-subagent-sessions)
- [Session index](#session-index)
- [Background subagents](#background-subagents)
- [Context compaction](#context-compaction)
- [Question custom answers](#question-custom-answers)
- [Load replay end](#load-replay-end)
- [Session fork point](#session-fork-point)
- [Presentation hints](#presentation-hints)
- [Removed keys](#removed-keys)
@@ -123,7 +126,8 @@ The `initialize` response of an AIR client carries the agent side of the extensi
"asyncTasks",
"recommendedValue",
"diffPatch",
"planFile"
"planFile",
"backgroundSubagents"
],
"goal": {
"version": 1,
@@ -137,26 +141,29 @@ The `initialize` response of an AIR client carries the agent side of the extensi
}
```
The agent list does not depend on the capabilities that AIR declares, except `sessionIndex`: the agent lists it, together with `sessionArchive`, `sessionRename` and `sessionListSubscribe`, only for a client that declared `sessionIndex`.
An extension is active only when the client declared its capability. The client does not declare `sessionArchive`, `sessionRename` and `sessionListSubscribe`.
The agent list does not depend on the capabilities that AIR declares, except `sessionIndex`: the agent lists it, together with `sessionArchive`, `sessionRename`, `sessionListSubscribe` and `sessionWorktrees`, only for a client that declared `sessionIndex`.
An extension is active only when the client declared its capability. The client does not declare `sessionArchive`, `sessionRename`, `sessionListSubscribe` and `sessionWorktrees`.
The response to a client that is not AIR has no `_meta.jetbrains` key.
### Capabilities
| Capability | Advertised | What the adapter does when the client declares it | Section |
| ------------------------ | ------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------- |
| `diffPatch` | yes | Sends an exact Git patch in the diff block of an Edit or a Write. | [Diff patch](#diff-patch) |
| `sessionFailure` | yes | Sends warnings and errors as typed transcript records. | [Session failure](#session-failure) |
| `recommendedValue` | yes | Replaces the `default` model and effort rows with concrete values and a recommendation. | [Recommended config values](#recommended-config-values) |
| `asyncTasks` | yes | Publishes background work that is not a subagent as async tasks. | [Async tasks](#async-tasks) |
| `agentFileChangeReport` | yes | Accepts a report request on `session/prompt` and sends the changed file list. | [Agent file-change report](#agent-file-change-report) |
| `nativeSubagentSessions` | yes | Reports an Agent or Task subagent as a native ACP child session. | [Native subagent sessions](#native-subagent-sessions) |
| `planFile` | yes | Sends the path of the plan file in place of the plan text of an ExitPlanMode. | [Plan file](#plan-file) |
| `sessionIndex` | on request | Pages, orders and annotates `session/list`; adds rename, archive and a list subscription. | [Session index](#session-index) |
| `sessionArchive` | with `sessionIndex` | Nothing on its own: the agent lists it to tell that it supports `_session/archive` and `_session/unarchive`. | [Archive](#archive) |
| `sessionRename` | with `sessionIndex` | Nothing on its own: the agent lists it to tell that it supports `_session/rename`. | [Rename](#rename) |
| `sessionListSubscribe` | with `sessionIndex` | Nothing on its own: the agent lists it to tell that it supports `_session/list/subscribe`. | [List subscription](#list-subscription) |
| `rawInputRendering` | no | Sends no display copy of readable input in `content`. The client renders `rawInput`. | [Tool call contract](#tool-call-contract) |
| Capability | Advertised | What the adapter does when the client declares it | Section |
| ------------------------ | ------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `diffPatch` | yes | Sends an exact Git patch in the diff block of an Edit or a Write. | [Diff patch](#diff-patch) |
| `sessionFailure` | yes | Sends warnings and errors as typed transcript records. | [Session failure](#session-failure) |
| `recommendedValue` | yes | Replaces the `default` model and effort rows with concrete values and a recommendation. | [Recommended config values](#recommended-config-values) |
| `asyncTasks` | yes | Publishes background work that is not a subagent as async tasks. | [Async tasks](#async-tasks) |
| `agentFileChangeReport` | yes | Accepts a report request on `session/prompt` and sends the changed file list. | [Agent file-change report](#agent-file-change-report) |
| `nativeSubagentSessions` | yes | Reports an Agent or Task subagent as a native ACP child session. | [Native subagent sessions](#native-subagent-sessions) |
| `planFile` | yes | Sends the path of the plan file in place of the plan text of an ExitPlanMode. | [Plan file](#plan-file) |
| `backgroundSubagents` | yes | Ends a prompt turn at its result while background subagents still run. | [Background subagents](#background-subagents) |
| `sessionIndex` | on request | Pages, orders and annotates `session/list`; adds rename, archive and a list subscription. | [Session index](#session-index) |
| `sessionArchive` | with `sessionIndex` | Nothing on its own: the agent lists it to tell that it supports `_session/archive` and `_session/unarchive`. | [Archive](#archive) |
| `sessionRename` | with `sessionIndex` | Nothing on its own: the agent lists it to tell that it supports `_session/rename`. | [Rename](#rename) |
| `sessionListSubscribe` | with `sessionIndex` | Nothing on its own: the agent lists it to tell that it supports `_session/list/subscribe`. | [List subscription](#list-subscription) |
| `sessionWorktrees` | with `sessionIndex` | Nothing on its own: the agent lists it to tell that rows carry `agentWorktree` for a worktree Claude Code made. | [List response](#list-response) |
| `loadReplayEnd` | no | Sends one `session_info_update` after the whole history replay of a loaded session. | [Load replay end](#load-replay-end) |
| `rawInputRendering` | no | Sends no display copy of readable input in `content`. The client renders `rawInput`. | [Tool call contract](#tool-call-contract) |
The adapter ignores `planContentDelta`.
Claude does not stream a plan, so the adapter never sends `contentDelta` (see [Plan file](#plan-file)).
@@ -185,11 +192,13 @@ The adapter sends these keys only to an AIR client. "AIR" in the Gate column mea
| `commandTitle` | Bash and PowerShell tool call `_meta.jetbrains.air` | the `description` input string | AIR |
| `subagent` | Agent and Task tool call `_meta.jetbrains.air` | `true` | AIR |
| `skill` | Skill tool call `_meta.jetbrains.air` | `{name, path?}` | AIR |
| `mcp` | MCP tool call `_meta.jetbrains.air` | `{server, tool}` | AIR |
| `contextCompaction` | compaction tool call or `compaction_update`, `_meta.jetbrains.air` | `{version: 1, trigger?, preTokens?, postTokens?, durationMs?, error?}` | AIR |
| `customAnswer` | elicitation schema property `_meta.jetbrains.air` | `{questionId, isCustomAnswer: true}` | AIR, when the client supports form elicitation |
| `kind` | session mode `_meta.jetbrains.air` and mode config option value `_meta.jetbrains.air` | `standard`, `plan`, `auto_review`, or `full_access` | AIR |
| `kind` | available command `_meta.jetbrains.air` of a skill | `skill` | AIR, when the skill's `SKILL.md` is found; not for a built-in command or an MCP prompt |
| `skillPath` | available command `_meta.jetbrains.air` of a skill | absolute path of the skill's `SKILL.md` | AIR, together with `kind: "skill"` |
| `loadReplayEnd` | `session_info_update._meta.jetbrains.air` | `true` | `loadReplayEnd` |
| `fork` | `session/fork` request `_meta.jetbrains.air` (client to agent) | `{version: 1, messageId, messageFingerprint?, messageOccurrence?}` | none |
## JetBrains shared keys
@@ -205,6 +214,21 @@ The adapter keeps them where they are.
| `steering` | `initialize` response `_meta.steering = {supported: true}` | The agent accepts `_session/steering` for a running turn. |
| `quota` | `PromptResponse._meta.quota` | Token usage of the turn, also split by model. |
| `authStatus` | `initialize` response `agentCapabilities._meta.authStatus` | The agent pushes `_auth/status_update`. The object carries no payload. |
| `promptSuggestions` | `session_info_update._meta.promptSuggestions = {suggestions: [{text}]}`, AIR only | The prompt the agent predicts for the next turn. An empty list clears it. |
## Auth source display
The connection-scoped `_auth/status_update` continues to report agent-owned authentication, not client-owned `providers/set` routing. When the active key source is `apiKeyHelper`, the adapter reads the effective helper command through SDK `resolveSettings` / the session's `SettingsManager`, including programmatic settings overrides.
A command containing the whole word `central` and the word sequence `proxy start` is displayed as:
```json
{ "authStatus": { "kind": "gateway", "label": "JetBrains Air Gateway" } }
```
Matching is case-insensitive and accepts multiple whitespace characters between `proxy` and `start`. The command's flags, URL, host and route are not checked. This is a weak display heuristic, not proof of configuration provenance, account ownership or entitlement. Inactive helpers and external cloud backends retain their existing classification. No helper command, key, headers, private route or inferred account is included in this payload.
Other active helpers use `label: "Anthropic API key"` and `detail: "API key helper"`; unknown key-source identifiers are not exposed as display text. CLI and SDK reads share this presentation, retain stale-probe protection, and suppress unchanged notifications.
## Zed conventions
@@ -215,6 +239,7 @@ An AIR client that declares `terminal_output_delta` gets appends instead of `ter
A command tool call then has `content: [{type: "terminal", terminalId}]` and `_meta.terminal_info = {terminal_id}`.
The output goes in `_meta.terminal_output = {terminal_id, data}`.
The end sends `_meta.terminal_exit = {terminal_id, exit_code, signal: null}`.
A command that ends without a result (a cancel stopped its subagent, say) gets the exit on the report that closes its tool call, with no exit code (`null`; AIR gets `1`).
- A client that declares neither `terminal_output` nor `terminal_output_delta` gets no terminal.
The command output is a `console` code block in `content`.
- A client that declares `clientCapabilities._meta["terminal-auth"]: true` gets `_meta["terminal-auth"] = {command, args, label}` on the terminal login methods.
@@ -307,10 +332,14 @@ For every tool, a client that is not AIR also gets these upstream fields:
| `Skill` | `kind: other`, `title: "Load skill: <name>"`. | `_meta.jetbrains.air.skill`. | none |
| `TodoWrite`, `TaskCreate`, `TaskUpdate`, `TaskList`, `TaskGet` | The stream reports them as a standard `plan` with entries. Only a permission request shows them as a tool call. | A plan that repeats the previous plan is not sent. | Every plan goes out. |
| `ReportFindings` | `kind: think`, `title: "Report <n> findings"`. | With `rawInputRendering`, no copy of the findings in `content`. | none |
| MCP tools (`mcp__*`) and other tools | `kind: other`, `title` is the tool name. The result text goes to `content`. | `_meta.is_mcp_tool_call` for MCP. | none |
| MCP tools (`mcp__*`) and other tools | `kind: other`, `title` is the tool name. The result text goes to `content`. | `_meta.is_mcp_tool_call` for MCP, and `_meta.jetbrains.air.mcp = {server, tool}`. | none |
| Context compaction | `compaction_update` when the client declares `session.compaction`. Otherwise a synthetic tool call. | Both forms carry `_meta.jetbrains.air.contextCompaction`. | See [Context compaction](#context-compaction). |
| Background work that is not a subagent | none | With `asyncTasks`, async task updates. | none |
`_meta.jetbrains.air.mcp` comes from the tool name `mcp__<server>__<tool>`, on `tool_call` and on a `tool_call_update` that changes it.
`server` is the part up to the next `__`. `tool` is the whole rest and can contain `__`, as in Claude Code.
A name that does not split (`mcp__x`) gets no `mcp` key, and `_meta.is_mcp_tool_call` still marks it.
## Diff patch
The diff patch extension lets the adapter send one compact Git patch instead of file text snapshots.
@@ -585,6 +614,7 @@ The client already has the rest of the tool call, because the adapter sends the
- `content` is present only with an exact preview patch for a `diffPatch` client.
- `locations` is present only when a valid `blockedPath` is not a location of the tool call yet.
- `_meta.claudeCode.mcpServer` names the MCP server of an `mcp__*` tool.
It is the server name as authored in the config. `_meta.jetbrains.air.mcp.server` on the tool call is the normalized name (characters outside `[a-zA-Z0-9_-]` replaced by `_`), so the two can differ.
A client that is not AIR gets the upstream request `toolCall`: `toolCallId`, `name`, `status: pending`, the whole `rawInput`, `title`, `kind`, `content`, and `locations`.
Its `_meta.claudeCode` carries `toolName`, and `parentToolUseId` for a subagent tool call, next to `mcpServer`.
@@ -800,6 +830,37 @@ While that command is pending, the adapter ignores late updates for the previous
A matching runtime `active_goal` stays authoritative and adds the iteration, reason, and timestamp.
A failed command restores the previous state.
## Predicted follow-up prompt
After a turn, the Claude Code CLI can predict the prompt the user is likely to send next.
The adapter asks for it with the SDK option `promptSuggestions: true` and forwards the one prediction of the turn.
ACP has no field for a composer prefill, so the prediction travels outside the AIR namespace:
```json
{
"sessionUpdate": "session_info_update",
"_meta": { "promptSuggestions": { "suggestions": [{ "text": "Run the tests" }] } }
}
```
The key names no vendor, so another client can read it.
The adapter sends it only to an AIR client; see the [compatibility rule](#compatibility-rule).
AIR shows the first non-blank `suggestions[].text` as gray text in an empty composer.
Tab fills the composer with it and sends nothing.
`suggestions: []` clears the prediction the client holds. The adapter clears it:
- when the next `session/prompt` arrives, because that turn's prediction is spent;
- on a `conversation_reset`, because the prediction was drawn from the transcript the reset dropped.
A prediction the next prompt outran — the user sent it before the adapter read the prediction off the SDK stream — is dropped instead of published.
The CLI predicts nothing on the first turn of a session, after an API error, in plan mode, and at the account's usage limit, so many turns end with no prediction.
A client must treat a turn without one as normal.
A caller can turn the whole feature off with `_meta.claudeCode.options.promptSuggestions: false` on `session/new`; a user can turn it off in Claude Code settings or with `CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false`.
## Recommended config values
The `recommendedValue` extension lets a client show concrete model and effort choices without an ambiguous `default` row.
@@ -936,6 +997,20 @@ When an announced task leaves that list, the adapter reports `stopped`.
A later terminal event can correct that state to `completed` or `failed`.
At shutdown, the adapter finishes each open task as `failed` or `stopped`.
### Autonomous turn
Claude Code can start a turn of its own after the prompt ended, for example to answer the notification of a finished background task.
ACP v1 has no session state, so the adapter publishes such a turn as an async task.
The task has the id `autonomous-turn-<uuid>`, the name `Claude is working`, `taskType: "turn"`, and `showInTranscript: false`.
Its description is the summary of the last task notification, when there is one.
The task starts at the SDK `running` state while no prompt is active or queued.
When the state stays `running`, as it does while background agents run, it starts at the first root output with no prompt open.
It ends `completed` at the result of the turn or at the SDK `idle` state, and `failed` at an error result.
A coalesced placeholder result (`num_turns: 0`) does not end it.
A prompt that the turn takes in ends the task, because that prompt owns the turn now.
A stop request interrupts the turn instead of stopping an SDK task.
It returns `{ "stopped": false }` while a prompt waits for its turn, because the interrupt would drop that prompt.
### Stop request
The client sends `_session/async_task/stop`:
@@ -1207,8 +1282,8 @@ The session index makes `session/list` a bounded, ordered page with row metadata
AIR declares `sessionIndex` in `_meta.jetbrains.air.capabilities`.
Unlike the other capabilities, the agent advertises `sessionIndex` only to a client that declared it, and never under ACP v2.
Exactly when it advertises `sessionIndex`, the agent also advertises `sessionArchive` (it supports `_session/archive` and `_session/unarchive`), `sessionRename` (it supports `_session/rename`) and `sessionListSubscribe` (it supports `_session/list/subscribe` and `_session/list/unsubscribe`).
The client does not declare these three; declaring them without `sessionIndex` enables nothing.
Exactly when it advertises `sessionIndex`, the agent also advertises `sessionArchive` (it supports `_session/archive` and `_session/unarchive`), `sessionRename` (it supports `_session/rename`), `sessionListSubscribe` (it supports `_session/list/subscribe` and `_session/list/unsubscribe`) and `sessionWorktrees` (rows carry `agentWorktree`, see [List response](#list-response)).
The client does not declare these four; declaring them without `sessionIndex` enables nothing.
Everything in this section applies only to such a client, except where [Delete and close](#delete-and-close) says otherwise.
A client without the capability gets the `session/list` of before: the SDK `listSessions` call, pages of 1000 by file mtime, `offset:N` cursors, and no row `_meta`.
It gets no new notification, and the adapter starts no watcher for it.
@@ -1276,6 +1351,7 @@ Each row carries `_meta.jetbrains.air` with flat row fields of the RFDs. `archiv
"lastPromptAt": "2026-10-06T09:57:40.000Z",
"model": "claude-opus-5-5",
"forkedFrom": "8f0c1d2e-0000-4000-8000-000000000000",
"agentWorktree": true,
"state": "idle",
"lastTurnEndedAt": "2026-10-06T09:58:10.000Z",
"cost": { "amount": 1.23, "currency": "USD" }
@@ -1285,10 +1361,19 @@ Each row carries `_meta.jetbrains.air` with flat row fields of the RFDs. `archiv
- `lastPromptAt` is the time of the last real user prompt in the transcript tail: prompt text (the predicate of the SDK title extractor) or an image or a document, but not a tool result, a meta or compact summary record, a slash command or an interrupt. When a long answer or tool output follows the last prompt, the tail window grows (256 KB, 1 MB, 4 MB) to find it; it is omitted only when the last 4 MB hold no prompt.
- `model` is the model of the last assistant message in the tail; messages the CLI makes up (`<synthetic>`) do not count.
- `forkedFrom` is the parent session id that the SDK `forkSession` and the CLI fork write on every copied record (`forkedFrom.sessionId`). Sessions started any other way have none.
- `agentWorktree` is `true` when the session runs in a linked git worktree that Claude Code made for it (`claude --worktree`, or the worktree tools), and omitted otherwise, never `false`. The transcript rule is the one of AIR's native Claude provider; the adapter adds the git check of the row's `cwd`:
- the transcript holds `worktree-state` records, `{"type":"worktree-state","worktreeSession":{originalCwd, worktreePath, worktreeName, worktreeBranch, originalBranch, originalHeadCommit, sessionId, hookBased?, enteredExisting?}}`, which the CLI writes when the session enters or leaves a worktree;
- the last line that parses as a `worktree-state` record decides, whatever it holds: the session is marked only when its `worktreeSession` is an object without `enteredExisting: true` (`enteredExisting` means a worktree that existed before, so not made for the session). Any other `worktreeSession` (`null`, which the CLI writes when the session leaves the worktree, a missing key, or a value that is no object) means not marked, and an older record does not count then. Only lines that are not JSON, such as the cut first line of a window, are skipped;
- the row's `cwd` must lie in a linked worktree: the adapter reads the git files, without running git. It takes the nearest `.git` above `cwd`, follows a `.git` file to its git dir, reads `commondir`, and requires the git dir to be `<git-common-dir>/worktrees/<name>`. This is checked on every `session/list` and on every row a subscription reads, not cached. The main checkout, a submodule and a directory outside a repository never get the field, and neither does a worktree the user made (it has no record). The adapter does not use `environment.isWorktree` or `git worktree list`;
- `hookBased` is not read: a worktree a `WorktreeCreate` hook made is marked only when its directory is a linked git worktree as above, so a hook that makes a plain directory or a checkout of another VCS gives no field. The repository is the nearest one to `cwd`: a session in a repository nested in a linked worktree is not marked, and one in a linked worktree of a nested repository is;
- the record is read from the 64 KB tail, else from the 64 KB head, and from the wider tail when the window grows to find the last prompt (see `lastPromptAt`). `claude --worktree` writes it at the start of the session, and the CLI writes it again with the other session metadata at the end of the transcript (on exit and as the transcript grows), so one of these windows almost always holds the latest one. A session that entered or left a worktree in the middle of a long transcript, with no metadata written since, keeps its old value until the CLI writes the metadata again;
- a session that made a worktree mid-session (EnterWorktree) is marked too: the CLI moves its transcript to the worktree (a `relocated` record), so the row's `cwd` is the worktree path, not the directory the session started in. The row's `cwd` is always the session's own directory, so the worktree path;
- a subscription does not send a row again when only git metadata changes, such as a manual `git worktree remove` or `git worktree move`: the field is checked again only when the transcript changes, or when the rescan finds that the worktrees of the scope changed and resolves the rows again. `session/list` checks it on every read.
- `state` is `running`, `idle`, `requires_action`, or `error`, never `unknown`. `error` means the session is not running and its last turn ended with an error; a user cancel or interrupt is no error, and a new turn clears it:
- a session this connection runs: the SDK `session_state_changed` state; `error` instead of `idle` when its last turn failed (an error result, `is_error` or an `error_*` subtype, that was not a cancel). A session whose query failed here (not a cancel) is reported as any other session, but `error` until its transcript shows a prompt or a turn end after the failure, unless a busy or waiting interactive CLI holds it;
- a session no live Claude Code process holds: `idle`, or `error` when its last turn ended with an API error (an assistant record with `isApiErrorMessage: true`, also when the CLI's turn-end records follow it, with no prompt after it);
- a session that an interactive CLI holds (not an `sdk-*` entrypoint) whose registry status is newer than the transcript: `busy` and `shell` are `running`, `waiting` is `requires_action`, `idle` is `idle`, or `error` after such an API error;
- any other held session whose last turn ended with an open background task: `running`, also after 10 minutes, until the registry status is `idle`. Claude Code wakes the session again when the task ends, so the session gets one `idle` at the last turn end, not one for each task. A background shell command (`toolUseResult.backgroundTaskId`) or an async sub-agent (`isAsync` with an `agentId`) opens a task. Its `<task-notification>` with a `<status>`, as a user record or a `queue-operation` `remove`, or the result of a `TaskStop` call closes it. A notification without a `<status>`, such as the check-in of a quiet shell or a `Monitor` event, keeps it open. Sidechain records open no task. The tail gives the tasks on top of those of an earlier scan when the file only grew within the tail; else the whole transcript is read, and only for such a held session. A background task that a sub-agent starts is not seen: it is only in the transcript of the sub-agent;
- any other held session: `idle` after a finished turn (an assistant `end_turn`, a user interrupt, or the CLI turn-end records), `error` after a turn that ended with an API error, `running` for an unfinished turn written in the last 10 minutes, and no state otherwise.
- The live processes come from `<config>/sessions/<pid>.json`, with the liveness rules of Claude Desktop: `kill(pid, 0)` (or `EPERM`), a matching `procStart`, no foreign `pidDomain`, and records older than 24 hours only with a matching `procStart`.
The adapter never reads the `.key` files there and never changes the directory.
@@ -1380,7 +1465,7 @@ From then on the agent sends `_session/list/changes` notifications for the sessi
- `cwd` is required and must be an absolute path; a missing, non-string or relative `cwd` is `-32602`. Other parameters are ignored.
- The scope is the sessions of `cwd` and of the same subdirectory in every existing linked worktree of its repository, as a list with `includeWorktrees: true` resolves it, in any archive state. Sidechain and subagent transcripts are left out, as in the list.
- `sessions` holds the full current row of each session in scope that appeared, or whose `title`, `lastPromptAt`, `state`, `lastTurnEndedAt`, `cost`, `model`, `forkedFrom` or `archived` changed since the last row sent for it on this subscription. A row is exactly the row of `session/list`, `_meta.jetbrains.air` included.
- `sessions` holds the full current row of each session in scope that appeared, or whose `title`, `lastPromptAt`, `state`, `lastTurnEndedAt`, `cost`, `model`, `forkedFrom`, `agentWorktree` or `archived` changed since the last row sent for it on this subscription. A row is exactly the row of `session/list`, `_meta.jetbrains.air` included.
- After subscribe, the first change of each session sends its row in full, even when only `updatedAt` changed. From then on, a change of `updatedAt` alone sends nothing; the new `updatedAt` comes with the next change.
- `removed` holds the ids of sessions that left the scope: deleted (the transcript is gone), or moved to a path outside it. It may name a session the client does not list (one it never listed, one the list hides, such as a session continued in another one when that one changes); the client ignores such an id.
- Archive and unarchive are row changes (`archived`): their title records are a transcript change. The agent applies no archive filter: the client filters.
@@ -1408,7 +1493,8 @@ It is idempotent, and an unknown id also returns `{}`.
- a `sessionIndex` client: deletes every transcript of the session (empty ones too) and every `<sessionId>/` directory, also one left without a transcript by an earlier failed delete. Anything left behind fails the request. A session that runs here is closed first. As with the CLI, the session is deleted also when another Claude Code process has it open. An unknown session is `-32002`.
- an AIR client without `sessionIndex`: archives the session instead of deleting it (the title records of [Archive](#archive)), because AIR uses delete for "Done" and can reopen the session. A session that runs here is closed first. The list hides archived sessions from such a client too: those whose custom title, as the SDK list reports it, is archived. A session without a transcript fails with the error of the SDK delete, as before.
A `sessionIndex` client sees such a session as archived (`archived: true`), as AIR's native Claude path does.
- any other client: the SDK delete, as before.
- any other client: the SDK delete. A session without a transcript (unknown, deleted before, or
never prompted) succeeds, as ACP asks.
`session/close` of a session that is not loaded returns `{}` for every client.
@@ -1426,21 +1512,22 @@ It is idempotent, and an unknown id also returns `{}`.
The session index is the `_meta` form of two ACP RFDs: "Session list extensions: limit, order, row metadata and change hints" and "Session Archive and Unarchive" (#2161).
Names and semantics follow them; the transport differs, and so do the `archived` list filter and the archive of a running session (see [Relation to ACP RFD #2161](#relation-to-acp-rfd-2161)):
| Extension | RFD |
| ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| capability `sessionIndex` (`_meta.jetbrains.air`) | `sessionCapabilities.list.limit` |
| capability `sessionArchive` (`_meta.jetbrains.air`) | `sessionCapabilities.archive` (#2161) |
| capability `sessionRename` (`_meta.jetbrains.air`) | `sessionCapabilities.setTitle` (#1987) |
| `_meta.jetbrains.air.list.limit` | `session/list` `limit` |
| `_meta.jetbrains.air.list.includeWorktrees` | `session/list` `includeWorktrees` |
| `_meta.jetbrains.air.list.archived` (`"unarchived"`, `"archived"`, `"all"`) | `session/list` `archived` (#2161), a boolean there |
| row `_meta.jetbrains.air.lastPromptAt`, `model`, `forkedFrom`, `state`, `lastTurnEndedAt`, `cost` | `SessionInfo` fields of the same names; the RFD `state` has no `error` |
| row `_meta.jetbrains.air.archived` | `SessionInfo.archived` (#2161) |
| `session_info_update` `_meta.jetbrains.air.archived` | `SessionInfoUpdate.archived` (#2161) |
| capability `sessionListSubscribe`, `_session/list/subscribe`, `_session/list/unsubscribe` | none |
| `_session/list/changes { subscriptionId, sessions, removed }` | none |
| `_session/archive`, `_session/unarchive` | `session/archive`, `session/unarchive` (#2161) |
| `_session/rename` | `session/set_title` (#1987) |
| Extension | RFD |
| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| capability `sessionIndex` (`_meta.jetbrains.air`) | `sessionCapabilities.list.limit` |
| capability `sessionArchive` (`_meta.jetbrains.air`) | `sessionCapabilities.archive` (#2161) |
| capability `sessionRename` (`_meta.jetbrains.air`) | `sessionCapabilities.setTitle` (#1987) |
| `_meta.jetbrains.air.list.limit` | `session/list` `limit` |
| `_meta.jetbrains.air.list.includeWorktrees` | `session/list` `includeWorktrees` |
| `_meta.jetbrains.air.list.archived` (`"unarchived"`, `"archived"`, `"all"`) | `session/list` `archived` (#2161), a boolean there |
| row `_meta.jetbrains.air.lastPromptAt`, `model`, `forkedFrom`, `state`, `lastTurnEndedAt`, `cost` | `SessionInfo` fields of the same names; the RFD `state` has no `error` |
| row `_meta.jetbrains.air.archived` | `SessionInfo.archived` (#2161) |
| `session_info_update` `_meta.jetbrains.air.archived` | `SessionInfoUpdate.archived` (#2161) |
| capability `sessionWorktrees`, row `_meta.jetbrains.air.agentWorktree` | none yet: proposed in the session list extensions RFD (agentclientprotocol/agent-client-protocol#2323) as `SessionInfo.agentWorktree`, with the capability named `agentWorktrees` there, not `sessionWorktrees` |
| capability `sessionListSubscribe`, `_session/list/subscribe`, `_session/list/unsubscribe` | none |
| `_session/list/changes { subscriptionId, sessions, removed }` | none |
| `_session/archive`, `_session/unarchive` | `session/archive`, `session/unarchive` (#2161) |
| `_session/rename` | `session/set_title` (#1987) |
### Relation to ACP RFD #2161
@@ -1455,6 +1542,45 @@ It differs in transport, until the RFD lands in the SDK, in the list filter, and
- the list parameter is `_meta.jetbrains.air.list.archived`, and the state is `_meta.jetbrains.air.archived` on rows and in `session_info_update`, not the `archived` fields;
- an AIR client without `sessionIndex` still archives with `session/delete` (see [Delete and close](#delete-and-close)), which the RFD asks clients not to do.
## Background subagents
A prompt turn can start a background Agent or Task subagent.
A SendMessage tool call can also resume a subagent that ended.
Claude Code sends the result of the turn when the main agent stops, also while such a subagent still runs.
### Hold without the capability
A client that does not declare `backgroundSubagents` gets the hold of issues #864 and #866.
The adapter keeps the `session/prompt` request open while a subagent that the turn started or resumed runs.
The subagent output, its permission requests and the followup summary of the model then go out inside the turn.
The turn ends at the followup result, at an `idle` with no live subagent, at `session/cancel`, or when the next prompt starts.
The hold protects a client that stops listening at the prompt response.
### Behavior with the capability
AIR declares `backgroundSubagents` in `initialize.clientCapabilities._meta.jetbrains.air.capabilities`.
The capability applies to ACP v1 only. ACP v2 keeps the hold, and the agent does not advertise the capability to ACP v2.
- The turn ends at its result with the stop reason of the result, usually `end_turn`.
- The subagent updates, permission requests and elicitations go out after the prompt response.
- The routing does not change: the native child session, else the root session with the parent tool call in `_meta.claudeCode.parentToolUseId`.
- The followup cycle after a subagent ends is an autonomous cycle. Its updates go out of a turn. Its result does not end a turn.
- A steer with no turn in flight starts a new turn, or returns `promptRequired` when the client asks for it.
- `session/cancel` with no turn works as before: the adapter interrupts the SDK query and reports each native subagent session as cancelled.
- The tool call of a running subagent does not fail the incomplete tool call check of the turn.
- The [agent file-change report](#agent-file-change-report) of the turn does not include an edit that a subagent makes after the result.
- An open permission request of a subagent counts as an open request for the next turn and for the steer priority.
### Plan approval out of a turn
The followup cycle can call ExitPlanMode. AIR then gets the permission request out of a turn.
The request has no clear-context option, because the fresh query must continue an ACP turn.
"No, keep planning" stops only the followup cycle. There is no turn to end.
A prompt turn owns the request when the turn is active, or when Claude Code asks before the echo of its prompt.
Then the request has the clear-context option.
A turn that took over a followup cycle also owns the request of that cycle, and the accepted plan continues in that turn.
## Context compaction
The adapter implements the ACP session compaction RFD for a client that declares `clientCapabilities.session.compaction`.
@@ -1510,6 +1636,32 @@ For AIR, the companion field also carries the same value under the root key `_me
Released AIR versions read only that key.
A client that is not AIR gets neither key.
## Load replay end
ACP has no signal that the history replay of `session/load` is over, other than the response.
The adapter replays the transcript while the Claude Code CLI starts, so the history can arrive long before the response.
The client declares `loadReplayEnd` in its capabilities. The agent does not advertise it.
After the history replay of the loaded session, the agent then sends one more update for that session:
```json
{
"sessionId": "<loaded session>",
"update": {
"sessionUpdate": "session_info_update",
"_meta": { "jetbrains": { "air": { "version": 1, "loadReplayEnd": true } } }
}
}
```
- The update has no `title` and no `updatedAt`. A `null` there would clear the value.
- It comes after every replayed update, the subagent history that the replay sends to the child sessions included, and before the `session/load` response.
- It comes also for an empty history.
- It comes for `session/load` of a new or an already live session, and for the ACP v2 `session/resume` with `replayFrom: {type: "start"}`.
- It does not come when the replay stops or fails. The replay stops when the CLI start fails during the replay.
- It does not come for a live update, and it does not come to a client without the capability.
- The `session/load` can still fail after it, for example when the CLI start fails after the replay. The event means that the recorded history arrived whole. The client decides what to do with the failed load.
## Session fork point
AIR can fork a session at one agent message.

View File

@@ -54,3 +54,14 @@ An ACP caller can provide `settings` via `_meta.claudeCode.options.settings` in
- The value must be valid JSON. Invalid JSON will cause session creation to fail with a parse error.
- Only `modelOverrides` and `availableModels` keys are read; other keys in the JSON are ignored.
- Both fields map directly to the Claude Agent SDK's `Settings` type.
## Models of a `providers/set` gateway
When an ACP client routes the agent through a gateway with `providers/set`, the adapter lists the gateway's models before `providers/set` returns, with the provider headers:
- `anthropic`: `GET {baseUrl}/v1/models`, paginated by `has_more`/`last_id`.
- `bedrock`: `GET {baseUrl}/foundation-models` (Bedrock `ListFoundationModels`, `modelSummaries[].modelId`). Only `anthropic.` ids count. The cross-region prefix `global` when listed, else that of the first listed id with one (`eu` for `eu.anthropic.claude-sonnet-5`), becomes the session's `ANTHROPIC_BEDROCK_REGION_PREFIX`, and ids with other prefixes, or without one, are dropped.
The model picker then offers only the SDK models whose resolved model the gateway lists, matched without dates, version suffixes (`-v1:0`), `-fast`, `@` suffixes, context hints (`[1m]`) and provider prefixes, after the `availableModels` allowlist above. On Bedrock, a model the CLI resolved to another region prefix is dropped as well. When the gateway does not list the model a session runs, that is the default model or a resumed session's last model, the session switches to the first remaining picker row.
A 4xx answer means the gateway has no listing: `providers/set` succeeds and nothing is narrowed. A failed listing (network error, a status other than 2xx or 4xx, unexpected body, the 10 s timeout, an invalid header, or a base URL with credentials) fails `providers/set` with a JSON-RPC internal error, and the previous provider stays in effect. A listing that matches no SDK model leaves the session with no model to offer: there is no model config option. Gateways set through `authenticate`, `vertex` providers, and native routing are not listed. Setting the same gateway again (the same protocol, base URL and headers) reuses its listing, unless it answered 4xx; `providers/set` and `providers/disable` apply in the order they arrive, and logout clears the routing after `claude auth logout`, including a `providers/set` still listing; `providers/disable` and logout drop it.

View File

@@ -82,10 +82,10 @@ import {
client as acpClient,
methods,
ndJsonStream,
} from "@agentclientprotocol/sdk";
} from "@agentclientprotocol/sdk/unstable";
// Type-only: Node's type stripping does not elide value imports, and these two
// exist only in the .d.ts — importing them normally is a runtime SyntaxError.
import type { AnyMessage, Stream } from "@agentclientprotocol/sdk";
import type { AnyMessage, Stream } from "@agentclientprotocol/sdk/unstable";
// ---------------------------------------------------------------- steering

View File

@@ -46,7 +46,7 @@ import {
ndJsonStream,
type PromptRequest,
type PromptResponse,
} from "@agentclientprotocol/sdk";
} from "@agentclientprotocol/sdk/unstable";
/** The steering extension method, per the ACP steering wire protocol. */
const STEERING_METHOD = "_session/steering";

524
package-lock.json generated
View File

@@ -1,69 +1,69 @@
{
"name": "@agentclientprotocol/claude-agent-acp",
"version": "0.89.1",
"version": "0.94.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@agentclientprotocol/claude-agent-acp",
"version": "0.89.1",
"version": "0.94.0",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "1.8.0",
"@anthropic-ai/claude-agent-sdk": "0.3.293",
"@agentclientprotocol/sdk": "2.0.0",
"@anthropic-ai/claude-agent-sdk": "0.3.303",
"diff": "9.0.0",
"zod": "4.6.5"
"zod": "4.6.7"
},
"bin": {
"claude-agent-acp": "dist/index.js"
},
"devDependencies": {
"@anthropic-ai/sdk": "0.132.1",
"@anthropic-ai/sdk": "0.135.0",
"@eslint/js": "10.0.1",
"@tsconfig/node22": "22.0.6",
"@types/node": "26.6.4",
"@typescript-eslint/eslint-plugin": "8.71.1",
"@typescript-eslint/parser": "8.71.1",
"@types/node": "26.6.5",
"@typescript-eslint/eslint-plugin": "8.71.2",
"@typescript-eslint/parser": "8.71.2",
"ajv": "8.20.0",
"ajv-formats": "3.0.1",
"eslint": "10.12.0",
"eslint": "10.13.0",
"eslint-config-prettier": "10.1.8",
"globals": "17.13.0",
"prettier": "3.9.9",
"prettier": "3.9.10",
"ts-node": "10.9.2",
"typescript": "6.0.3",
"vitest": "5.0.3"
"vitest": "5.0.5"
},
"engines": {
"node": ">=22"
}
},
"node_modules/@agentclientprotocol/sdk": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-1.8.0.tgz",
"integrity": "sha512-fbP7qHZsFyTJVT0/RPrcWJ8An26qLXHEuGCHHEKDyxD1xpXV+Ejyh2LAE6vBAFePkpVjZeeohnbgvuEhZ7wLMg==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-2.0.0.tgz",
"integrity": "sha512-Za5gWOPQ44aBOdegDJZ+OjCAPn7POc0cw8BlFJQ1mmUY1OoUWxRi3+QZT4gdZnyEWgKXyfeGRb0Oo1ag9YkQ1w==",
"license": "Apache-2.0",
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
"zod": "^3.25.76 || ^4.0.0"
}
},
"node_modules/@anthropic-ai/claude-agent-sdk": {
"version": "0.3.293",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.293.tgz",
"integrity": "sha512-DVfn46SmpfB7xaKuUHsAC+qwY27hOLHNbvF7FoNOAXLZPRTeCr68SjhBjlFOtk+jz5Nf7pfUwnIUDFcdVih6Dg==",
"version": "0.3.303",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.303.tgz",
"integrity": "sha512-XBql04V40xr+fmGJh8AmIpqRZKFNrs99BzXzzSVlx+uE5NjFCzKmkNKTdMD0DQtDvFJlijOLoh1siaH+au9Psw==",
"license": "SEE LICENSE IN README.md",
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.293",
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.293",
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.293",
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.293",
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.293",
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.293",
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.293",
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.293"
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.303",
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.303",
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.303",
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.303",
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.303",
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.303",
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.303",
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.303"
},
"peerDependencies": {
"@anthropic-ai/sdk": ">=0.93.0",
@@ -72,9 +72,9 @@
}
},
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": {
"version": "0.3.293",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.293.tgz",
"integrity": "sha512-E2rwiiHr3LwYzBuTROVDpJkts4QmYCrW39ZIN1qixVSNQhSEiz5LjaKxzrwq0xgPgQDU0vd2fLMG1k72BgD24w==",
"version": "0.3.303",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.303.tgz",
"integrity": "sha512-QAWV9Zd7i7FQDaUSwZNVXJirqCS9CSF4aPwbkGwLvLH83qcfFOmnyxWUg1H7dU6tJFLOCyDLQzc3xzUA4dakoA==",
"cpu": [
"arm64"
],
@@ -85,9 +85,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": {
"version": "0.3.293",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.293.tgz",
"integrity": "sha512-UJD8kX1XDhMqs/0LLxD012h+S9U3Q8XPbRSNcnPQEMNAoEF+lh898xPbVnkUgPKl3jUOUyqYRwFRwDiUJD+GJg==",
"version": "0.3.303",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.303.tgz",
"integrity": "sha512-o931musaryuq04fMaEjceCEmgsBrLh39WvwsQWmv070h9jXS8EmWRFpdSILv1xrUFXiukRSq8yUDRVzg+FvTug==",
"cpu": [
"x64"
],
@@ -98,9 +98,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": {
"version": "0.3.293",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.293.tgz",
"integrity": "sha512-B3M5gw6pPH/ckMtrLtGg4ZY1adLVnPoXkEV+Ruu0j/gdFzwuLWR64Spa0RCsbo7BXVN6w5NTqkuZk+HBfsPUZA==",
"version": "0.3.303",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.303.tgz",
"integrity": "sha512-c04i9i1BU3j9G3D7XdVzXTdM3/sDpQH+1o8jNcMPnztOTYbNFmH/cacgeX9x3N112r+5VhCEHXGILoe5GxVxeg==",
"cpu": [
"arm64"
],
@@ -111,9 +111,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": {
"version": "0.3.293",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.293.tgz",
"integrity": "sha512-l5Nfwox69bBMEccSQfPQFOq9hbSqQIMsi4H/UXaMSsqgnxunXuKEBXb5klXZJwk4vUt/NzeqAjB/UFbKqW29HQ==",
"version": "0.3.303",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.303.tgz",
"integrity": "sha512-a/OsLTEx9ly9hC26cXidg9mtTMA00SQnEiTN9P4IPNj64uSp/7uBnlWl0VeUoVFbgKYYq6XrGRcD6F1ilQsYxg==",
"cpu": [
"arm64"
],
@@ -124,9 +124,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": {
"version": "0.3.293",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.293.tgz",
"integrity": "sha512-MtqXROSQiuqOCNXfC4k3/3DVfgTNLEdaT5jp/omaZeiPtnRtfCz2KTtBTVG4IwbEDFaKsqfzPsPFzN7X6avwkg==",
"version": "0.3.303",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.303.tgz",
"integrity": "sha512-lsm1sX2+9Wos8glBbn86M8leFuK60LKB1cJdS0b2HiTSwiDCqpUKYRGJNUlW1JdUTwJB5nDv6S8/FOJExm1/tw==",
"cpu": [
"x64"
],
@@ -137,9 +137,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": {
"version": "0.3.293",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.293.tgz",
"integrity": "sha512-5N30l2Y17ZV9q0TGCMXtrkTleX+RVtUMoOL+48qQ7ZrKWGYYBtVDS59cIXDv+TAGqoyYVmklw0W2Sy4dA40BwQ==",
"version": "0.3.303",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.303.tgz",
"integrity": "sha512-yixAa3ZOZd1hiI4mpoukzT9TnuDCzSEHyh7pHcbYTRmdRE2y8bAOmI0eiMFJPivnG4iyGhS2C4XNAe6hAjl30w==",
"cpu": [
"x64"
],
@@ -150,9 +150,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": {
"version": "0.3.293",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.293.tgz",
"integrity": "sha512-WvPf8cXna446bTx2U4WVQc5+iUgDn94ve8aauvWwcG8n9oa+1wNCKNGFxk8HvS7DQJhF8dfrCbcF068uNFqYNw==",
"version": "0.3.303",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.303.tgz",
"integrity": "sha512-AnEseiw5E1JF3XRaGIZN880V/i2OxCaTOQJF9F1+oELleSdvLnCUxCE6/baGvaZjrgvffO9YbYChBpgUKj9j7A==",
"cpu": [
"arm64"
],
@@ -163,9 +163,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": {
"version": "0.3.293",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.293.tgz",
"integrity": "sha512-SVaIDHtpCwM0r0m9jzue1U7yktIZ1eJhR1O3e0UCHYTJluaIXty5m6DzqhG4rjnEoNlEAl9rQLqPigS/R5+IiQ==",
"version": "0.3.303",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.303.tgz",
"integrity": "sha512-j5kABFEQ6x6T5AB/glmn0r9VwZRbzcH+nSEOWglB69ODNfkZA2btjcz9r794+2BLqJ9QAyswiYWcOX4sDz/RmQ==",
"cpu": [
"x64"
],
@@ -176,11 +176,12 @@
]
},
"node_modules/@anthropic-ai/sdk": {
"version": "0.132.1",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.132.1.tgz",
"integrity": "sha512-Dg+Rd0effi+4FyNwgEx6JXl9COefsZ3C7BOPSKYIWboQOEIi9HXtu1WBdND0DIgMF7Qt1vuCSjsbs7sjUkzJuQ==",
"version": "0.135.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.135.0.tgz",
"integrity": "sha512-WWrdL57JhU5KoynpTjt9GXK6k7ud9nPw2uw/Trn+oBXh6s0KpjhAccWZm2Qxw08ARYG1UDQj5iC+JO42j1rt/g==",
"license": "MIT",
"dependencies": {
"@opentelemetry/api": "^1.4.1",
"json-schema-to-ts": "^3.1.1",
"standardwebhooks": "^1.0.0"
},
@@ -358,9 +359,9 @@
}
},
"node_modules/@hono/node-server": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.4.tgz",
"integrity": "sha512-2g1qeHl4BduwDn6L8nkAvDrPUWPMlkQ//a5NhP/zIyaXKMjZNRV+cG9Csu+BiJFf6+68pqzjf4Hi5sJGKHuS1g==",
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.5.tgz",
"integrity": "sha512-MgP/I06GdvsilDspMk/qmRzpk8LOUStP6jFAbSRi40W09DhoyUn+Mj7eyKICPqeYKybOjVFLVIAQzvu1bfIa7Q==",
"license": "MIT",
"peer": true,
"engines": {
@@ -529,10 +530,19 @@
}
}
},
"node_modules/@opentelemetry/api": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz",
"integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==",
"license": "Apache-2.0",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@oxc-project/types": {
"version": "0.153.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.153.0.tgz",
"integrity": "sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==",
"version": "0.154.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.154.0.tgz",
"integrity": "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -541,9 +551,9 @@
}
},
"node_modules/@rolldown/binding-android-arm-eabi": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.13.tgz",
"integrity": "sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==",
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.14.tgz",
"integrity": "sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==",
"cpu": [
"arm"
],
@@ -559,9 +569,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.13.tgz",
"integrity": "sha512-6LEoIrGy5vvCd2boNnv2soPSUZj/PRvhp8R/t9KlX+mIZxThIE/ZqxrGXxmuNX0FuvQFvLdZr7S4HrYpYBVhEQ==",
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.14.tgz",
"integrity": "sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==",
"cpu": [
"arm64"
],
@@ -577,9 +587,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.13.tgz",
"integrity": "sha512-CaJSRzlQ6Q+zkDMVA9OkVxBjVoPRCDEE3v0FSJ9kIQBgRjFApA3Si4ijhbNVUji7ZJt+PfO0e46qy4nPInHUPA==",
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.14.tgz",
"integrity": "sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==",
"cpu": [
"arm64"
],
@@ -595,9 +605,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.13.tgz",
"integrity": "sha512-e4r340I5hxSzJkD3YJBxgzq/rJ3wwxwdLLb2FPt1Zpyem20nrQPT11DDEEkazwxaHumH8wpglsp/Ozgj/wT6mw==",
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.14.tgz",
"integrity": "sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==",
"cpu": [
"x64"
],
@@ -613,9 +623,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.13.tgz",
"integrity": "sha512-hoBWDZ4/K/mJKVMbsgUd1jfpA2DiOrJ54mpegObU9TBHBBrtX4FLIXJJQRDPZ/2Dn91GvPob+Z2hwKJzf4E5SQ==",
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.14.tgz",
"integrity": "sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==",
"cpu": [
"x64"
],
@@ -631,9 +641,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.13.tgz",
"integrity": "sha512-oF9fZu3vTWnTiVAkDFxU9PJLt1nh20K9RRpuHsR7ZnE2L7XookBbDBVLojCVSLGbvtSlxeUNnjAj6DbNsKyzbg==",
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.14.tgz",
"integrity": "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==",
"cpu": [
"arm"
],
@@ -649,9 +659,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.13.tgz",
"integrity": "sha512-DTpb/+jMiqWMPgphn4ksW1zW+XLJ6dJJ9FMvnIk9I291LhCctMPhMYJrP5t9p6C9qa3KXadDdUEZHBaPh3kNxQ==",
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.14.tgz",
"integrity": "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==",
"cpu": [
"arm64"
],
@@ -667,9 +677,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.13.tgz",
"integrity": "sha512-xU+Kvv9N2A3SY35zFh1FWWNjxbadE/BKeqnkXe7vScU4GZsw757BlVmDLITZEq8DPpk6m7uoDYkK7P+x/uU5Ow==",
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.14.tgz",
"integrity": "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==",
"cpu": [
"arm64"
],
@@ -685,9 +695,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.13.tgz",
"integrity": "sha512-XMk/hnhh25oQ756Lclb1Q11h4ll0xC0Esa5w4Vzp+mAE2S3AGJgeM0SonrIgY8fHhjarb6hcoTuwoFG0VVP2ig==",
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.14.tgz",
"integrity": "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==",
"cpu": [
"ppc64"
],
@@ -703,9 +713,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.13.tgz",
"integrity": "sha512-U1BoTHYmlywE2n3HUWjzZ+8EQsKr+OwpxYuDWaubx42tUL8QSL7nOqRz/NVHanibbFP3M1Rxuprz/wNcbMKjqw==",
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.14.tgz",
"integrity": "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==",
"cpu": [
"s390x"
],
@@ -721,9 +731,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.13.tgz",
"integrity": "sha512-J4cm80Pc6wEwV5AZ42o62sIQgDk+Y7R2s646jIwuvO+4V46QB58N4vVuMPWCBaNgursXLpmUWJV//WQAkPvHBQ==",
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.14.tgz",
"integrity": "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==",
"cpu": [
"x64"
],
@@ -739,9 +749,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.13.tgz",
"integrity": "sha512-W0euZS40U4V6JkmEnVCIX9mcsL6cbXQ4X8c2w+MMOIjFg68TXUW+u3RT+2e1t1pv9mg/EP3pDtK3POXdQ7Rxyw==",
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.14.tgz",
"integrity": "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==",
"cpu": [
"x64"
],
@@ -757,9 +767,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.13.tgz",
"integrity": "sha512-do/AF/4n9MRshia3a5aXoy8xM2c6mgfT6D7H/dY6my5z+hfCIR4ab7mk4IRj++nsEQpKy39kd0DQnDq+r692tw==",
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.14.tgz",
"integrity": "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==",
"cpu": [
"arm64"
],
@@ -775,9 +785,9 @@
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.13.tgz",
"integrity": "sha512-6FtUf2cJZobutJdxnGYX8z0vtbgqU0b2v8vfBhkXb9wp2MppMFtkunE+IaOntyi01rkIQoPW2GHABtfzsC0BRQ==",
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.14.tgz",
"integrity": "sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==",
"cpu": [
"arm64"
],
@@ -793,9 +803,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.13.tgz",
"integrity": "sha512-UKhFaZTeTcbKrb3iU82m17jVcPEAoqDkCN5MBqgPyZSwp/cQmib21crTS0DdH7T1B7wZRWEI89YQAOJMet5yhg==",
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.14.tgz",
"integrity": "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==",
"cpu": [
"x64"
],
@@ -899,9 +909,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "26.6.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz",
"integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==",
"version": "26.6.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz",
"integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -909,17 +919,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.71.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.1.tgz",
"integrity": "sha512-nNlBf6HuotqMgZvSkWD5hNdemiBIxQ2NmrlgfcUIeZRs+BbakjscCQRq/Hb74EFVnFiSWWdL12JruqwBy3ERDw==",
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.2.tgz",
"integrity": "sha512-K72/pvT7qpm8q3k8G5u93NCSN1RcLxX5h7Gw0bQpbMXviBHeY6Nx1WEwmk9wP+eKTSQ8ecxvcgt/ZX/rB938xQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.71.1",
"@typescript-eslint/type-utils": "8.71.1",
"@typescript-eslint/utils": "8.71.1",
"@typescript-eslint/visitor-keys": "8.71.1",
"@typescript-eslint/scope-manager": "8.71.2",
"@typescript-eslint/type-utils": "8.71.2",
"@typescript-eslint/utils": "8.71.2",
"@typescript-eslint/visitor-keys": "8.71.2",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -932,22 +942,22 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.71.1",
"@typescript-eslint/parser": "^8.71.2",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.71.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.1.tgz",
"integrity": "sha512-oM/5sAqz/l1v/zYK1qTOPrn2+JIrBGC1V6uNYucRO5gh6eT9vWJ/RhPbGE59byvZEUQ4VP6XkLBaS5Jti5jpvw==",
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.2.tgz",
"integrity": "sha512-aTtmZjp/x3gsGHkDs04/1I8kNogBJkpQaeGaN0PfiG43NzDEmW2n+OP5im9wfsvyX0yP1UZWBrAGtDBfH9jEVw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.71.1",
"@typescript-eslint/types": "8.71.1",
"@typescript-eslint/typescript-estree": "8.71.1",
"@typescript-eslint/visitor-keys": "8.71.1",
"@typescript-eslint/scope-manager": "8.71.2",
"@typescript-eslint/types": "8.71.2",
"@typescript-eslint/typescript-estree": "8.71.2",
"@typescript-eslint/visitor-keys": "8.71.2",
"debug": "^4.4.3"
},
"engines": {
@@ -963,14 +973,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.71.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.1.tgz",
"integrity": "sha512-Oijc9RUsohHjncg6iGi7gCtjnEjYt9FWv9u6ygK8uKcHK5eLjLAkqERQ9BaytbpceOFdk6kifPCS/zAZE+A99g==",
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.2.tgz",
"integrity": "sha512-Pd68GsLVh5ZXlcEccFPdhSOvtHZ/2WU0XsA8zmXOS3F158lVlcTLhfQ6qvtL1RzbMie0Kp8xtbVwAg4nejKPHw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.71.1",
"@typescript-eslint/types": "^8.71.1",
"@typescript-eslint/tsconfig-utils": "^8.71.2",
"@typescript-eslint/types": "^8.71.2",
"debug": "^4.4.3"
},
"engines": {
@@ -985,14 +995,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.71.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.1.tgz",
"integrity": "sha512-1YUDZXdTnXLlob03SFHBipdyMBsDVm7zoSz0ytTJ39dk5J1TgsTK03VlR/dLXG/RLLUrPcYbD4rp23BmHxHP0w==",
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.2.tgz",
"integrity": "sha512-YF4wgg5iUX5Obywoac27B56FhozMNFhdJSbh11H5GBAwmBtfyRhmgTQqW9tRNNmHDvsZ2CX6UGzoz+YfYn2Leg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.71.1",
"@typescript-eslint/visitor-keys": "8.71.1"
"@typescript-eslint/types": "8.71.2",
"@typescript-eslint/visitor-keys": "8.71.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1003,9 +1013,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.71.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.1.tgz",
"integrity": "sha512-oTvkml5SxXhgg+WWKhod5qWQICDXtOB+/Hi9VdPKFAANJ2b7YOgMffgvzXuqog7R6A/JtrzRf128dsxummyWhg==",
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.2.tgz",
"integrity": "sha512-jz49TAWokm/7yWD9/o837nYax9DyALxIPOImdImvVzo6canBCZYzin8zJyqNR5k1j9K1DE0bVvr0XYB+99uAdg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1020,15 +1030,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.71.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.1.tgz",
"integrity": "sha512-kH3t3jZCYv2PZSBEWPKTariLrCaoF1CLoPitYFBJOhAGEwWnSRAtGEifrfrpBa4K9JlwWNZotgA5w3/wLSk/FQ==",
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.2.tgz",
"integrity": "sha512-cC8UaalCViJkb3rH/xLIvidEwoFRtMZMUqDjjJMGEKaTFwYRHYBcoczlEGHuvkHEO8svRDrgZ0yQgOArFmwang==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.71.1",
"@typescript-eslint/typescript-estree": "8.71.1",
"@typescript-eslint/utils": "8.71.1",
"@typescript-eslint/types": "8.71.2",
"@typescript-eslint/typescript-estree": "8.71.2",
"@typescript-eslint/utils": "8.71.2",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -1045,9 +1055,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.71.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.1.tgz",
"integrity": "sha512-ybYnPTUwg3VQQOFEiBPLs8Jxl1ry2vXdn1kVKguomBbvxpetdu3MKTNDqFNUrlG8ylP9MpZosmXMFYlGQHk1oQ==",
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.2.tgz",
"integrity": "sha512-y5Ukl3oVp7e/VGxqIoEXoeb/Roj71PAJ4WG/N/RGj+eeDvKRLM15kGr9MijLbN0i03S52zxu31Z98E5gWChJKw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1059,16 +1069,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.71.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.1.tgz",
"integrity": "sha512-pSKSEK1JJpHs6KiKVtABCE4iJK5d7FLFzDXQNBQ2oRY7LEd3UwOt3TZemTgu9syTIf56dJWlTn/pdmrO9oKUXQ==",
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.2.tgz",
"integrity": "sha512-DgNOEsl4pdYzwSL+N/Cl33tX1Hlw3FvId73LnKXfq7jnrF2viOEn7X+P7RBkkSWCVm92gnWd28GIC5vJuLREeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.71.1",
"@typescript-eslint/tsconfig-utils": "8.71.1",
"@typescript-eslint/types": "8.71.1",
"@typescript-eslint/visitor-keys": "8.71.1",
"@typescript-eslint/project-service": "8.71.2",
"@typescript-eslint/tsconfig-utils": "8.71.2",
"@typescript-eslint/types": "8.71.2",
"@typescript-eslint/visitor-keys": "8.71.2",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -1087,16 +1097,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.71.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.1.tgz",
"integrity": "sha512-CBjT6gfAz3DW2j9Q4moFnqt208Aq+506sVGXfpRcyAeqn0EpayL0mjdWoCO2t3RC7GewNrFxdeehuEaPg6DMzQ==",
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.2.tgz",
"integrity": "sha512-RyKa1G1M9rKHDhrk+HJtiF1MvLD5Ux2JW3TSkzw+02pRALdpJb+QzG1Y5em+LvCgzGwgg8H7mBRFKljNz7ImPw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.71.1",
"@typescript-eslint/types": "8.71.1",
"@typescript-eslint/typescript-estree": "8.71.1"
"@typescript-eslint/scope-manager": "8.71.2",
"@typescript-eslint/types": "8.71.2",
"@typescript-eslint/typescript-estree": "8.71.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1111,13 +1121,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.71.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.1.tgz",
"integrity": "sha512-0GxiUGqMU0qXJt58cBsiG3DP8lUmBaFo2vChtUX+CT7PnR3v/VGLLrzVP4K9r1lNiZXjHYefMBbPunplr09v6w==",
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.2.tgz",
"integrity": "sha512-zozlN0UA32bSG2zkKAgQyRYSbEISVoyU6vvkKYo2aSny74D8Jh4e+POkJ6Lk0dlezZi5x7TtD+88Xb06IB8nhg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.71.1",
"@typescript-eslint/types": "8.71.2",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -1142,22 +1152,22 @@
}
},
"node_modules/@vitest/mocker": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.3.tgz",
"integrity": "sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.5.tgz",
"integrity": "sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "0.3.31",
"@vitest/spy": "5.0.3",
"@vitest/spy": "5.0.5",
"estree-walker": "^3.0.3",
"magic-string": "^1.2.3"
"magic-string": "^1.4.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"msw": "^2.4.9",
"msw": "^2.4.9 || ^3.0.0",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
@@ -1181,9 +1191,9 @@
}
},
"node_modules/@vitest/spy": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.3.tgz",
"integrity": "sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.5.tgz",
"integrity": "sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==",
"dev": true,
"license": "MIT",
"funding": {
@@ -1418,9 +1428,9 @@
}
},
"node_modules/content-disposition": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
"integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-2.0.1.tgz",
"integrity": "sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==",
"license": "MIT",
"peer": true,
"engines": {
@@ -1647,9 +1657,9 @@
}
},
"node_modules/eslint": {
"version": "10.12.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.12.0.tgz",
"integrity": "sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==",
"version": "10.13.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.13.0.tgz",
"integrity": "sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==",
"dev": true,
"license": "MIT",
"workspaces": [
@@ -1931,16 +1941,16 @@
}
},
"node_modules/express": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/express/-/express-5.3.0.tgz",
"integrity": "sha512-qCqy1xSoaugxOD4IVHb1ZWdkvCF2W3aE2okKouWj8VwJ+ztskSiXoBnCSJqfKT+cVPJhuJqe9YLbY425+Zh00g==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
"content-disposition": "^1.0.0",
"content-type": "^1.0.5",
"body-parser": "^2.3.0",
"content-disposition": "^2.0.1",
"content-type": "^2.0.0",
"cookie": "^0.7.1",
"cookie-signature": "^1.2.1",
"debug": "^4.4.0",
@@ -1956,14 +1966,14 @@
"on-finished": "^2.4.1",
"once": "^1.4.0",
"parseurl": "^1.3.3",
"proxy-addr": "^2.0.7",
"qs": "^6.14.0",
"proxy-addr": "^2.0.8",
"qs": "^6.16.0",
"range-parser": "^1.2.1",
"router": "^2.2.0",
"send": "^1.1.0",
"serve-static": "^2.2.0",
"statuses": "^2.0.1",
"type-is": "^2.0.1",
"type-is": "^2.1.0",
"vary": "^1.1.2"
},
"engines": {
@@ -1994,6 +2004,20 @@
"express": ">= 4.11"
}
},
"node_modules/express/node_modules/content-type": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
"integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -2021,9 +2045,9 @@
"license": "Unlicense"
},
"node_modules/fast-uri": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
"integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz",
"integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==",
"funding": [
{
"type": "github",
@@ -2368,9 +2392,9 @@
"peer": true
},
"node_modules/ip-address": {
"version": "10.7.3",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.3.tgz",
"integrity": "sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==",
"version": "10.7.4",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.4.tgz",
"integrity": "sha512-DWBMKyeM25D70nrIiTkgQth59/G+naKOTep6huF9j+GmeK3DMD60Zvxg5d5qQnz+38AHgou6ean2LECX+lrKDg==",
"license": "MIT",
"peer": true,
"engines": {
@@ -2964,9 +2988,9 @@
}
},
"node_modules/obug": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz",
"integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz",
"integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==",
"dev": true,
"funding": [
"https://github.com/sponsors/sxzz",
@@ -3122,9 +3146,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.29",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz",
"integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==",
"version": "8.5.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz",
"integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==",
"dev": true,
"funding": [
{
@@ -3143,7 +3167,7 @@
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.19",
"nanoid": "^3.3.20",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.2"
},
@@ -3162,9 +3186,9 @@
}
},
"node_modules/prettier": {
"version": "3.9.9",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz",
"integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==",
"version": "3.9.10",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz",
"integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==",
"dev": true,
"license": "MIT",
"bin": {
@@ -3226,9 +3250,9 @@
"license": "MIT"
},
"node_modules/qs": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
"integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.16.1.tgz",
"integrity": "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==",
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
@@ -3282,14 +3306,14 @@
}
},
"node_modules/rolldown": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.13.tgz",
"integrity": "sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ==",
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.14.tgz",
"integrity": "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@oxc-project/types": "=0.153.0",
"@oxc-project/types": "=0.154.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
@@ -3299,21 +3323,21 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm-eabi": "1.2.13",
"@rolldown/binding-android-arm64": "1.2.13",
"@rolldown/binding-darwin-arm64": "1.2.13",
"@rolldown/binding-darwin-x64": "1.2.13",
"@rolldown/binding-freebsd-x64": "1.2.13",
"@rolldown/binding-linux-arm-gnueabihf": "1.2.13",
"@rolldown/binding-linux-arm64-gnu": "1.2.13",
"@rolldown/binding-linux-arm64-musl": "1.2.13",
"@rolldown/binding-linux-ppc64-gnu": "1.2.13",
"@rolldown/binding-linux-s390x-gnu": "1.2.13",
"@rolldown/binding-linux-x64-gnu": "1.2.13",
"@rolldown/binding-linux-x64-musl": "1.2.13",
"@rolldown/binding-openharmony-arm64": "1.2.13",
"@rolldown/binding-win32-arm64-msvc": "1.2.13",
"@rolldown/binding-win32-x64-msvc": "1.2.13"
"@rolldown/binding-android-arm-eabi": "1.2.14",
"@rolldown/binding-android-arm64": "1.2.14",
"@rolldown/binding-darwin-arm64": "1.2.14",
"@rolldown/binding-darwin-x64": "1.2.14",
"@rolldown/binding-freebsd-x64": "1.2.14",
"@rolldown/binding-linux-arm-gnueabihf": "1.2.14",
"@rolldown/binding-linux-arm64-gnu": "1.2.14",
"@rolldown/binding-linux-arm64-musl": "1.2.14",
"@rolldown/binding-linux-ppc64-gnu": "1.2.14",
"@rolldown/binding-linux-s390x-gnu": "1.2.14",
"@rolldown/binding-linux-x64-gnu": "1.2.14",
"@rolldown/binding-linux-x64-musl": "1.2.14",
"@rolldown/binding-openharmony-arm64": "1.2.14",
"@rolldown/binding-win32-arm64-msvc": "1.2.14",
"@rolldown/binding-win32-x64-msvc": "1.2.14"
}
},
"node_modules/router": {
@@ -3543,9 +3567,9 @@
"license": "MIT"
},
"node_modules/tinybench": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.1.tgz",
"integrity": "sha512-IAbQaPJIIdhVxi0mqy9lez1wEwje0UQf9F1vwtbelEG5Nbgn0nPOxCvU+mNeNS9gyJTAhrHp74CbUaSynQpZcQ==",
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz",
"integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3767,17 +3791,17 @@
}
},
"node_modules/vite": {
"version": "8.3.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.3.tgz",
"integrity": "sha512-cTAldKPImjg6c+gk48U19POPn3GCBzZwpdsN8ZMEEcbpes+6/wvfqUd0C2y3qYY4wsj8PwgFwrZ/1jBPVttMSg==",
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz",
"integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"lightningcss": "^1.33.0",
"picomatch": "^4.0.7",
"postcss": "^8.5.28",
"rolldown": "~1.2.11",
"postcss": "^8.5.29",
"rolldown": "~1.2.12",
"tinyglobby": "^0.2.17"
},
"bin": {
@@ -3846,23 +3870,23 @@
}
},
"node_modules/vitest": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.3.tgz",
"integrity": "sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.5.tgz",
"integrity": "sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/mocker": "5.0.3",
"chai": "^6.2.2",
"@vitest/mocker": "5.0.5",
"chai": "^6.3.0",
"es-module-lexer": "^2.3.2",
"expect-type": "^1.4.0",
"magic-string": "^1.2.3",
"obug": "^2.1.4",
"magic-string": "^1.4.3",
"obug": "^3.0.0",
"picomatch": "^4.0.7",
"std-env": "^4.2.0",
"tinybench": "^6.1.4",
"tinyexec": "^1.3.0",
"std-env": "^4.3.0",
"tinybench": "^6.2.2",
"tinyexec": "^1.3.1",
"tinyglobby": "^0.2.17",
"why-is-node-running": "3.2.1"
},
@@ -3879,12 +3903,12 @@
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^22.0.0 || >=24.0.0",
"@vitest/browser-playwright": "5.0.3",
"@vitest/browser-preview": "5.0.3",
"@vitest/browser-playwright": "5.0.5",
"@vitest/browser-preview": "5.0.5",
"@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0",
"@vitest/coverage-istanbul": "5.0.3",
"@vitest/coverage-v8": "5.0.3",
"@vitest/ui": "5.0.3",
"@vitest/coverage-istanbul": "5.0.5",
"@vitest/coverage-v8": "5.0.5",
"@vitest/ui": "5.0.5",
"happy-dom": "*",
"jsdom": "*",
"vite": "^6.4.0 || ^7.0.0 || ^8.0.0"
@@ -3997,9 +4021,9 @@
}
},
"node_modules/zod": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz",
"integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==",
"version": "4.6.7",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz",
"integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"

View File

@@ -3,7 +3,7 @@
"publishConfig": {
"access": "public"
},
"version": "0.89.1",
"version": "0.94.0",
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
"main": "dist/lib.js",
"types": "dist/lib.d.ts",
@@ -65,26 +65,26 @@
"node": ">=22"
},
"dependencies": {
"@agentclientprotocol/sdk": "1.8.0",
"@anthropic-ai/claude-agent-sdk": "0.3.293",
"@agentclientprotocol/sdk": "2.0.0",
"@anthropic-ai/claude-agent-sdk": "0.3.303",
"diff": "9.0.0",
"zod": "4.6.5"
"zod": "4.6.7"
},
"devDependencies": {
"@anthropic-ai/sdk": "0.132.1",
"@anthropic-ai/sdk": "0.135.0",
"@eslint/js": "10.0.1",
"@tsconfig/node22": "22.0.6",
"@types/node": "26.6.4",
"@typescript-eslint/eslint-plugin": "8.71.1",
"@typescript-eslint/parser": "8.71.1",
"@types/node": "26.6.5",
"@typescript-eslint/eslint-plugin": "8.71.2",
"@typescript-eslint/parser": "8.71.2",
"ajv": "8.20.0",
"ajv-formats": "3.0.1",
"eslint": "10.12.0",
"eslint": "10.13.0",
"eslint-config-prettier": "10.1.8",
"globals": "17.13.0",
"prettier": "3.9.9",
"prettier": "3.9.10",
"ts-node": "10.9.2",
"typescript": "6.0.3",
"vitest": "5.0.3"
"vitest": "5.0.5"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ import type {
ClientCapabilities,
SessionCapabilities,
SessionNotification,
} from "@agentclientprotocol/sdk";
} from "@agentclientprotocol/sdk/unstable";
import {
AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY,
clientSupportsAirCapability,
@@ -17,7 +17,8 @@ export { AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY } from "./air-extension.js";
* - `rfd`: the subagents RFD (agentclientprotocol/agent-client-protocol#1992,
* as revised): `subagent_update` and `session_message`, which the SDK types.
* A client that declares `clientCapabilities.subagents` and is not AIR gets
* it. (ACP v2 clients do not get subagents yet.)
* it. (An ACP v2 client gets it without declaring anything, since subagents
* are part of v2's baseline.)
* - `air`: the earlier draft of that RFD, which AIR implements:
* `subagent_spawned` and `subagent_state_update`, typed below. AIR gets it,
* whichever capability it declares (`docs/air-extensions.md`).

View File

@@ -1,5 +1,10 @@
export const AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY = "nativeSubagentSessions";
export const AIR_ASYNC_TASKS_CAPABILITY = "asyncTasks";
/**
* A prompt turn ends at its result also while its background subagents run.
* The subagent updates and requests then go out after the prompt response.
*/
export const AIR_BACKGROUND_SUBAGENTS_CAPABILITY = "backgroundSubagents";
export const AIR_SESSION_FAILURE_CAPABILITY = "sessionFailure";
export const AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY = "recommendedValue";
export const AIR_CUSTOM_INSTRUCTIONS_CAPABILITY = "customInstructions";
@@ -34,11 +39,26 @@ export const AIR_SESSION_RENAME_CAPABILITY = "sessionRename";
* {@link AIR_SESSION_INDEX_CAPABILITY} is; the client does not declare it.
*/
export const AIR_SESSION_LIST_SUBSCRIBE_CAPABILITY = "sessionListSubscribe";
/**
* Session index rows carry `agentWorktree: true` when the session runs in a
* linked git worktree that Claude Code made for it. Advertised exactly when
* {@link AIR_SESSION_INDEX_CAPABILITY} is; the client does not declare it.
*/
export const AIR_SESSION_WORKTREES_CAPABILITY = "sessionWorktrees";
/**
* The client wants to know when the history replay of a loaded session is
* whole. After the last replayed update, the agent sends one
* `session_info_update` with `_meta.jetbrains.air.loadReplayEnd: true` (see
* {@link AIR_LOAD_REPLAY_END_KEY}). The client declares it; the agent does not
* advertise it.
*/
export const AIR_LOAD_REPLAY_END_CAPABILITY = "loadReplayEnd";
/** The `_meta.jetbrains.air` keys that the ACP tool call contract defines. */
export const AIR_COMMAND_TITLE_KEY = "commandTitle";
export const AIR_SUBAGENT_KEY = "subagent";
export const AIR_SKILL_KEY = "skill";
export const AIR_MCP_KEY = "mcp";
export const AIR_CONTEXT_COMPACTION_KEY = "contextCompaction";
export const AIR_GOAL_KEY = "goal";
export const AIR_KIND_KEY = "kind";
@@ -46,6 +66,8 @@ export const AIR_KIND_KEY = "kind";
export const AIR_SKILL_PATH_KEY = "skillPath";
export const AIR_PERMISSION_KEY = "permission";
export const AIR_CUSTOM_ANSWER_KEY = "customAnswer";
/** The `_meta.jetbrains.air` key of the update that ends a whole history replay. */
export const AIR_LOAD_REPLAY_END_KEY = "loadReplayEnd";
/**
* The root `_meta` spelling of {@link AIR_CUSTOM_ANSWER_KEY} that released
* AIR versions read. Remove it when the ACP registry no longer serves AIR

View File

@@ -1,4 +1,5 @@
import type { ClientCapabilities } from "@agentclientprotocol/sdk";
import { randomUUID } from "node:crypto";
import type { ClientCapabilities } from "@agentclientprotocol/sdk/unstable";
import type { AcpSessionNotification, AsyncTaskState } from "./acp-subagents.js";
import { AIR_ASYNC_TASKS_CAPABILITY, clientSupportsAirCapability } from "./air-extension.js";
import { noticeOrTranscriptUpdate } from "./session-notices.js";
@@ -39,6 +40,12 @@ type AsyncTask = {
terminalSource?: TerminalSource;
/** The optional fields that the client holds now, as JSON. */
published: Map<string, string>;
/**
* Whether the task stands for a model turn that Claude Code started on its
* own, see {@link AsyncTaskRuntime.autonomousTurnStarted}. The SDK knows no
* such task, so its background task list never ends it.
*/
autonomousTurn: boolean;
};
type TaskIdentity = { taskId?: unknown; task_id?: unknown };
@@ -121,6 +128,10 @@ export class AsyncTaskRuntime {
* The oldest entries are dropped.
*/
private readonly outputPathsByToolCall = new Map<string, string[]>();
/** The task of the autonomous turn that runs now. */
private autonomousTurn?: AsyncTask;
/** The summary of the last task notification, which usually starts the next autonomous turn. */
private lastNotificationSummary?: string;
constructor(
readonly enabled: boolean,
@@ -300,6 +311,7 @@ export class AsyncTaskRuntime {
const state = taskState(message.status);
if (!taskId || !state || state === "running" || state === "paused") return;
this.lastNotificationSummary = nonBlankString(message.summary) ?? this.lastNotificationSummary;
const task = this.task(taskId);
const wasTerminal = isTerminal(task.state);
const correctsLevelState = wasTerminal && task.terminalSource === "level";
@@ -363,6 +375,7 @@ export class AsyncTaskRuntime {
if (
(task.announced || task.held) &&
!task.ignored &&
!task.autonomousTurn &&
!isTerminal(task.state) &&
!live.has(task.id)
) {
@@ -378,6 +391,7 @@ export class AsyncTaskRuntime {
}
async finishAll(state: Extract<AsyncTaskState, "failed" | "stopped">): Promise<void> {
this.autonomousTurn = undefined;
const errors: unknown[] = [];
for (const task of this.tasks.values()) {
if ((task.announced || task.held) && !task.ignored && !isTerminal(task.state)) {
@@ -412,6 +426,7 @@ export class AsyncTaskRuntime {
const task = this.tasks.get(taskId);
if (!task || !task.announced || task.ignored || task.stopAnnounced) return;
task.stopAnnounced = true;
if (this.autonomousTurn === task) this.autonomousTurn = undefined;
// No terminal summary: a stopped task leaves the Async Tasks panel at once,
// so anything said there is said to nobody.
await this.finish(task, "stopped", undefined, "event");
@@ -438,6 +453,47 @@ export class AsyncTaskRuntime {
);
}
/**
* Publishes a model turn that Claude Code started with no prompt open, for
* example to answer a task notification. ACP v1 has no session state, and
* the client already got the stop reason of its last prompt, so this task is
* the only sign that the agent works. The tool calls of the turn are in the
* transcript already, so the task is not shown there. Does nothing while
* such a task is open.
*/
async autonomousTurnStarted(): Promise<void> {
if (!this.enabled || this.autonomousTurn) return;
const task = this.task(`autonomous-turn-${randomUUID()}`);
task.autonomousTurn = true;
task.startedObserved = true;
task.name = "Claude is working";
task.taskType = "turn";
task.description =
this.lastNotificationSummary ?? "Claude Code started a turn without a prompt.";
task.showInTranscript = false;
this.lastNotificationSummary = undefined;
this.autonomousTurn = task;
await this.announce(task, { withoutToolCall: true });
}
/** Whether the task of an autonomous turn is open. */
get autonomousTurnOpen(): boolean {
return this.autonomousTurn !== undefined;
}
/** Ends the task of the autonomous turn that runs now, if there is one. */
async autonomousTurnEnded(state: "completed" | "failed"): Promise<void> {
const task = this.autonomousTurn;
if (!task) return;
this.autonomousTurn = undefined;
await this.finish(task, state, undefined, "event");
}
/** Whether the task stands for an autonomous turn, which a stop interrupts. */
isAutonomousTurn(taskId: string): boolean {
return this.tasks.get(taskId)?.autonomousTurn === true;
}
/** Whether a task of the tool call went to the background, so the client gets it. */
isBackgroundedToolCall(toolCallId: string): boolean {
for (const task of this.tasks.values()) {
@@ -488,6 +544,8 @@ export class AsyncTaskRuntime {
clear(): void {
this.tasks.clear();
this.outputPathsByToolCall.clear();
this.autonomousTurn = undefined;
this.lastNotificationSummary = undefined;
}
/**
@@ -520,6 +578,7 @@ export class AsyncTaskRuntime {
stopAnnounced: false,
state: "running",
published: new Map(),
autonomousTurn: false,
};
this.tasks.set(taskId, task);
return task;

View File

@@ -103,6 +103,13 @@ const NOT_LOGGED_IN: AuthStatus = {
label: "Not logged in",
};
const API_KEY_SOURCE_DETAILS = new Map([
["apiKeyHelper", "API key helper"],
["env", "env"],
["ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY"],
["ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_AUTH_TOKEN"],
]);
/** Display names for the non-firstParty backends, where auth lives outside the
* agent (AWS credentials, gcloud ADC, …). */
const EXTERNAL_PROVIDER_LABELS: Record<string, string> = {
@@ -162,7 +169,10 @@ export function accountInfoHasIdentitySignal(account: AccountInfo | undefined):
/** Session-time mapping from the SDK's `AccountInfo`, the richest source when
* it is populated. Returns `undefined` when the account carries no identity
* signal, so callers keep the status they already know. */
export function fromAccountInfo(account: AccountInfo | undefined): AuthStatus | undefined {
export function fromAccountInfo(
account: AccountInfo | undefined,
apiKeyHelper?: string,
): AuthStatus | undefined {
if (!account || !accountInfoHasIdentitySignal(account)) {
return undefined;
}
@@ -172,13 +182,14 @@ export function fromAccountInfo(account: AccountInfo | undefined): AuthStatus |
apiKeySource: account.apiKeySource,
email: account.email,
organization: account.organization,
apiKeyHelper,
});
}
/** Pre-session mapping from `claude auth status --json` stdout. Returns
* `undefined` when the output is not the expected JSON object, so callers can
* report "not known" instead of guessing. */
export function fromCliStatus(stdout: string): AuthStatus | undefined {
export function fromCliStatus(stdout: string, apiKeyHelper?: string): AuthStatus | undefined {
let parsed: unknown;
try {
parsed = JSON.parse(stdout);
@@ -205,6 +216,7 @@ export function fromCliStatus(stdout: string): AuthStatus | undefined {
apiKeySource: status.apiKeySource,
email: status.email,
organization: status.orgName,
apiKeyHelper,
});
}
@@ -224,6 +236,7 @@ function mapAuthFields(fields: {
apiKeySource?: string;
email?: string;
organization?: string;
apiKeyHelper?: string;
}): AuthStatus {
const { apiProvider, subscriptionType, apiKeySource, email, organization } = fields;
@@ -237,11 +250,19 @@ function mapAuthFields(fields: {
};
}
if (apiKeySource) {
if (
apiKeySource === "apiKeyHelper" &&
typeof fields.apiKeyHelper === "string" &&
/\bcentral\b/i.test(fields.apiKeyHelper) &&
/\bproxy\s+start\b/i.test(fields.apiKeyHelper)
) {
return { kind: "gateway", label: "JetBrains Air Gateway" };
}
const detail = API_KEY_SOURCE_DETAILS.get(apiKeySource);
return {
kind: "api_key",
label: "Anthropic API key",
// The source ("apiKeyHelper", "env", …) is the specifics line.
detail: apiKeySource,
...(detail ? { detail } : {}),
};
}
if (subscriptionType) {

View File

@@ -1,4 +1,4 @@
import type { NewSessionRequest, SessionConfigOption } from "@agentclientprotocol/sdk";
import type { NewSessionRequest, SessionConfigOption } from "@agentclientprotocol/sdk/unstable";
import type {
EffortLevel,
Options,
@@ -44,6 +44,7 @@ export type ClearContextSession<Turn extends ClearContextTurn = ClearContextTurn
turnQueue?: Turn[];
pendingExitPlanContextReset?: ClearContextReset;
contextUsedTokens?: number;
sentUserUuids?: Set<string>;
input: { push(message: SDKUserMessage): unknown };
};
@@ -158,6 +159,7 @@ export async function continuePlanInFreshContext<
}
assertRestartActive(freshSession);
(freshSession.sentUserUuids ??= new Set()).add(turn.promptUuid);
freshSession.input.push(host.continuationMessage(sessionId, reset.plan, turn.promptUuid));
assertRestartActive(freshSession);
host.ensureConsumer(freshSession, sessionId);

View File

@@ -0,0 +1,74 @@
/**
* The client-provided system prompt of the unstable `systemPrompt` field on
* `session/new`, `session/load`, `session/resume`, and `session/fork`.
*
* - `append` keeps the Claude Code preset and adds the text after it, the
* SDK's `{ type: "preset", preset: "claude_code", append }`.
* - `override` replaces the Claude Code preset with the text, the SDK's custom
* string prompt. Tools, MCP servers, permissions, and settings (including the
* CLAUDE.md files that `settingSources` loads) are unchanged.
*
* The CLI records the system prompt of a conversation on its first request
* and reuses it on resume, so a different `systemPrompt` on a later
* `session/resume` takes effect at the next compaction. The RFD allows this
* deferral.
*/
import type { Options } from "@anthropic-ai/claude-agent-sdk";
import {
RequestError,
type SessionCapabilities,
type SystemPrompt,
} from "@agentclientprotocol/sdk/unstable";
/** The `sessionCapabilities.systemPrompt` the agent advertises: both modes. */
export const SYSTEM_PROMPT_CAPABILITY = {
mode: { override: {} },
} satisfies NonNullable<SessionCapabilities["systemPrompt"]>;
export type ClientSystemPrompt = { mode: "append" | "override"; text: string };
/**
* The mode and text of a `systemPrompt`, or `undefined` when the request has
* none. Throws `invalid_params` for a mode other than `append` and `override`
* and for a content block other than text: the RFD requires an error, and no
* session, for a prompt the agent cannot apply.
*/
export function clientSystemPrompt(
systemPrompt: SystemPrompt | null | undefined,
): ClientSystemPrompt | undefined {
if (systemPrompt == null) return undefined;
const { mode, content } = systemPrompt;
if (mode !== "append" && mode !== "override") {
throw RequestError.invalidParams(
{ systemPrompt: { mode } },
`Unsupported systemPrompt mode: ${mode}`,
);
}
const text: string[] = [];
for (const block of content) {
if (block.type !== "text") {
throw RequestError.invalidParams(
{ systemPrompt: { content: { type: block.type } } },
`systemPrompt content must be text, but received a ${block.type} block`,
);
}
text.push(block.text);
}
return { mode, text: text.join("\n\n") };
}
/**
* The SDK `systemPrompt` option for a client system prompt. `customInstructions`
* (AIR's `customInstructions` on `session/new` metadata) still follows the
* client text in both modes.
*/
export function sdkSystemPrompt(
prompt: ClientSystemPrompt,
customInstructions: string | undefined,
): NonNullable<Options["systemPrompt"]> {
const text = [prompt.text, customInstructions].filter((part) => part).join("\n\n");
if (prompt.mode === "override") return text;
return text
? { type: "preset", preset: "claude_code", append: text }
: { type: "preset", preset: "claude_code" };
}

View File

@@ -1,4 +1,4 @@
import { ClientCapabilities, SessionNotification } from "@agentclientprotocol/sdk";
import { ClientCapabilities, SessionNotification } from "@agentclientprotocol/sdk/unstable";
import {
ContextCompactionMetadata,
createContextCompactionMeta,

View File

@@ -1,5 +1,5 @@
import { ToolCallContent, ToolCallLocation } from "@agentclientprotocol/sdk";
import type * as v2 from "@agentclientprotocol/sdk/experimental/v2";
import { ToolCallContent, ToolCallLocation } from "@agentclientprotocol/sdk/unstable";
import type * as v2 from "@agentclientprotocol/sdk/experimental/v2/unstable";
import { structuredPatch } from "diff";
import { constants } from "node:fs";
import { type FileHandle, open } from "node:fs/promises";

View File

@@ -1,12 +1,12 @@
import { randomUUID } from "node:crypto";
import { CreateElicitationResponse } from "@agentclientprotocol/sdk";
import { CreateElicitationResponse } from "@agentclientprotocol/sdk/unstable";
import type {
CreateElicitationRequest,
ElicitationAcceptAction,
ElicitationPropertySchema,
ElicitationSchema,
EnumOption,
} from "@agentclientprotocol/sdk";
} from "@agentclientprotocol/sdk/unstable";
import type { ElicitationRequest, ElicitationResult } from "@anthropic-ai/claude-agent-sdk";
import type {
AskUserQuestionInput,

View File

@@ -1,4 +1,4 @@
import type { SessionNotification } from "@agentclientprotocol/sdk";
import type { SessionNotification } from "@agentclientprotocol/sdk/unstable";
import type { SDKResultMessage } from "@anthropic-ai/claude-agent-sdk";
import {
type ClearContextCoordinatorHost,

View File

@@ -19,6 +19,10 @@ export type FileChangeReportTurnState = {
export type NativeFileChangeReportTurn = {
promptUuid: string;
fileChangeReport?: FileChangeReportTurnState;
/** Raw `file_path`/`notebook_path` values of the file-tool edits that
* subagents of this turn completed. Checkpoints do not track files that
* only a subagent edited. */
editedPaths?: Set<string>;
};
export type FileChangeReportWorkspace = {
@@ -94,6 +98,41 @@ export function agentFileChangeReportMeta(
return withAirMeta(undefined, AGENT_FILE_CHANGE_REPORT_CAPABILITY, result);
}
const FILE_EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
/**
* Adds to `turn.editedPaths` the `file_path` (`notebook_path` for NotebookEdit)
* of each successful file-tool edit among the tool results of a subagent
* message. Root-agent messages (`parentToolUseId === null`) are skipped: the
* checkpoint already tracks their files. Must run before `toAcpNotifications`
* drops the tool use cache entries.
*/
export function recordSubagentFileEdits(
turn: (NativeFileChangeReportTurn & { settled: boolean }) | null | undefined,
parentToolUseId: string | null,
content: unknown,
toolUses: Record<string, { name: string; input: unknown } | undefined>,
): void {
if (parentToolUseId === null || !turn || turn.settled || !turn.fileChangeReport) return;
if (!Array.isArray(content)) return;
for (const block of content) {
if (
block?.type !== "tool_result" ||
block.is_error === true ||
typeof block.tool_use_id !== "string"
) {
continue;
}
const toolUse = toolUses[block.tool_use_id];
if (!toolUse || !FILE_EDIT_TOOLS.has(toolUse.name)) continue;
const input = toolUse.input as Record<string, unknown> | undefined;
const editedPath = toolUse.name === "NotebookEdit" ? input?.notebook_path : input?.file_path;
if (typeof editedPath === "string" && editedPath.length > 0) {
(turn.editedPaths ??= new Set()).add(editedPath);
}
}
}
export function createNativeFileChangeReporter(
options: NativeFileChangeReporterOptions,
): NativeFileChangeReporter {
@@ -101,7 +140,7 @@ export function createNativeFileChangeReporter(
const timeoutMs = options.timeoutMs ?? AGENT_FILE_CHANGE_REPORT_TIMEOUT_MS;
const requestIds = new Set<string>();
const publish = async (state: FileChangeReportTurnState, result: AgentFileChangeReportResult) => {
async function publish(state: FileChangeReportTurnState, result: AgentFileChangeReportResult) {
if (state.phase === "finished") return;
state.phase = "finished";
try {
@@ -109,24 +148,55 @@ export function createNativeFileChangeReporter(
} catch (error) {
options.logError(`Failed to publish file-change report ${state.requestId}: ${error}`);
}
};
}
const finishUnavailable = async (
async function finishUnavailable(
state: FileChangeReportTurnState,
reason: FileChangeReportUnavailableReason,
) => {
) {
await publish(state, {
version: 1,
requestId: state.requestId,
status: "unavailable",
reason,
});
};
}
const report = async (
async function publishReported(state: FileChangeReportTurnState, reportedPaths: string[]) {
const normalized = normalizeReportedPaths(reportedPaths, workspace);
await publish(
state,
fitReportedAgentFileChangeReport({
version: 1,
requestId: state.requestId,
status: "reported",
paths: normalized.paths,
// Checkpoints miss Bash mutations. Subagent file-tool edits are added
// from the message stream, but the list is still not exhaustive.
declaredComplete: false,
truncated: normalized.truncated,
}),
);
}
// Used when the checkpoint preview fails: the subagent edits seen in the
// stream are still worth reporting.
async function finishWithoutPreview(
state: FileChangeReportTurnState,
editedPaths: Set<string> | undefined,
reason: FileChangeReportUnavailableReason,
) {
if (editedPaths && editedPaths.size > 0) {
await publishReported(state, [...editedPaths]);
} else {
await finishUnavailable(state, reason);
}
}
async function report(
turn: NativeFileChangeReportTurn | null | undefined,
query: Pick<Query, "rewindFiles">,
) => {
) {
const state = turn?.fileChangeReport;
if (!state || state.phase !== "requested") return;
state.phase = "collecting";
@@ -145,35 +215,22 @@ export function createNativeFileChangeReporter(
}),
]);
if (preview === null) {
await finishUnavailable(state, "timeout");
await finishWithoutPreview(state, turn.editedPaths, "timeout");
return;
}
if (!preview.canRewind || !Array.isArray(preview.filesChanged)) {
await finishUnavailable(state, "invalidOutput");
await finishWithoutPreview(state, turn.editedPaths, "invalidOutput");
return;
}
const normalized = normalizeReportedPaths(preview.filesChanged, workspace);
await publish(
state,
fitReportedAgentFileChangeReport({
version: 1,
requestId: state.requestId,
status: "reported",
paths: normalized.paths,
// Checkpoints cover Claude file tools, but not every mutation source
// (notably Bash and most subagents), so this list is never exhaustive.
declaredComplete: false,
truncated: normalized.truncated,
}),
);
await publishReported(state, [...preview.filesChanged, ...(turn.editedPaths ?? [])]);
} catch (error) {
options.logError(`Failed to inspect Claude file checkpoint ${turn.promptUuid}: ${error}`);
await finishUnavailable(state, "providerError");
await finishWithoutPreview(state, turn.editedPaths, "providerError");
} finally {
if (timer) clearTimeout(timer);
}
};
}
return {
request(meta) {

View File

@@ -1,4 +1,8 @@
import { ForkSessionRequest, ForkSessionResponse, RequestError } from "@agentclientprotocol/sdk";
import {
ForkSessionRequest,
ForkSessionResponse,
RequestError,
} from "@agentclientprotocol/sdk/unstable";
import {
forkSession as forkClaudeSession,
getSessionMessages,

223
src/gateway-models.ts Normal file
View File

@@ -0,0 +1,223 @@
/**
* Model discovery for a gateway set via `providers/set`.
*
* An organization can restrict which models its gateway serves, and the
* gateway's listing returns only those: `GET /v1/models` for an `anthropic`
* gateway, `GET /foundation-models` (Bedrock `ListFoundationModels`) for a
* `bedrock` one. The adapter lists them once when the provider is set, and the
* model picker then offers only the SDK models the gateway listed (see
* `narrowToGatewayModels` in `session-model.ts`). A listing that fails fails
* `providers/set` with a {@link GatewayListingError}.
*/
const LISTING_TIMEOUT_MS = 10_000;
const ANTHROPIC_PAGE_LIMIT = "1000";
const MAX_LISTING_PAGES = 20;
const MAX_LOGGED_BODY_CHARS = 500;
const ANTHROPIC_VERSION = "2023-06-01";
/**
* The bearer token `createEnvForProvider` sets as `ANTHROPIC_AUTH_TOKEN` and
* `AWS_BEARER_TOKEN_BEDROCK`, so the CLI sends `Authorization: Bearer acp-proxy`
* unless the provider headers replace it. The listing sends the same, so the
* gateway sees the same auth as on chat requests.
*/
export const PROVIDER_AUTH_TOKEN_PLACEHOLDER = "acp-proxy";
/**
* The cross-region inference profile prefixes the CLI knows:
* `eu.anthropic.claude-sonnet-5`. `ANTHROPIC_BEDROCK_REGION_PREFIX` accepts all
* of them but `us-gov`, which the CLI takes only from a `us-gov-*` AWS region;
* a gateway listing only `us-gov.*` ids would then get an empty picker. That
* case is theoretical, so it is left as is.
*/
const BEDROCK_REGION_PREFIXES = ["us", "eu", "apac", "jp", "au", "us-gov", "global"];
type ListingLogger = { log: (...args: any[]) => void; error: (...args: any[]) => void };
type ListingPage = { ids: string[]; nextQuery?: Record<string, string> };
/**
* A gateway listing that failed: a network error, a status other than 2xx or
* 4xx, a body that is not the expected listing, or the 10 s timeout. The
* message names the request path and the reason, never a header value.
*/
export class GatewayListingError extends Error {
override name = "GatewayListingError";
}
/**
* Lists the model ids an `anthropic` gateway at `baseUrl` serves, following
* `has_more` / `last_id` pagination. Returns `undefined` when the gateway
* answers 4xx: it has no listing, and nothing is narrowed. Throws a
* {@link GatewayListingError} when the listing fails. Header values are never
* logged.
*/
export async function listGatewayModels(
baseUrl: string,
providerHeaders: Record<string, string>,
logger: ListingLogger,
): Promise<string[] | undefined> {
return listPages(
`${baseUrl.replace(/\/+$/, "")}/v1/models`,
{ limit: ANTHROPIC_PAGE_LIMIT },
listingHeaders(providerHeaders, { "anthropic-version": ANTHROPIC_VERSION }),
logger,
(body) => {
const page = body as { data?: unknown; has_more?: unknown; last_id?: unknown };
if (!Array.isArray(page.data)) return undefined;
return {
ids: stringField(page.data, "id"),
nextQuery:
page.has_more === true && typeof page.last_id === "string"
? { limit: ANTHROPIC_PAGE_LIMIT, after_id: page.last_id }
: undefined,
};
},
);
}
/**
* Lists the Anthropic models a `bedrock` gateway at `baseUrl` serves
* (`GET /foundation-models`, Bedrock `ListFoundationModels`, one page), the
* listing AI Router answers on its Bedrock route. Returns the listed ids, such
* as `global.anthropic.claude-sonnet-5-5`; `undefined` and the errors are
* those of {@link listGatewayModels}.
*/
export async function listBedrockModels(
baseUrl: string,
providerHeaders: Record<string, string>,
logger: ListingLogger,
): Promise<string[] | undefined> {
const ids = await listPages(
`${baseUrl.replace(/\/+$/, "")}/foundation-models`,
{},
listingHeaders(providerHeaders, {}),
logger,
(body) => {
const page = body as { modelSummaries?: unknown };
if (!Array.isArray(page.modelSummaries)) return undefined;
return { ids: stringField(page.modelSummaries, "modelId") };
},
);
return ids?.filter((id) => id.includes("anthropic."));
}
/** The cross-region prefix of a Bedrock model id or ARN, if it has one. */
export function bedrockRegionPrefix(modelId: string): string | undefined {
const id = modelId.slice(modelId.lastIndexOf("/") + 1).toLowerCase();
return BEDROCK_REGION_PREFIXES.find((prefix) => id.startsWith(`${prefix}.anthropic.`));
}
/**
* Picks the region of the listed Bedrock models: `global` when the gateway
* lists it, else the prefix of the first id that has one. A gateway that lists
* `eu.*` and `us.*` ids gets the prefix that comes first, and the ids of the
* other prefixes are dropped, so the picker offers only models the CLI can
* call under that prefix. Ids without a prefix (`anthropic.claude-…`) are
* dropped too once a prefix is picked; without any prefix every id is kept.
*/
export function selectBedrockRegion(modelIds: readonly string[]): {
regionPrefix?: string;
modelIds: string[];
} {
const prefixes = modelIds.map(bedrockRegionPrefix);
const regionPrefix = prefixes.includes("global")
? "global"
: prefixes.find((prefix) => prefix !== undefined);
if (!regionPrefix) return { modelIds: [...modelIds] };
return {
regionPrefix,
modelIds: modelIds.filter((id) => bedrockRegionPrefix(id) === regionPrefix),
};
}
// Provider headers win over the defaults, matched case-insensitively, the way
// `ANTHROPIC_CUSTOM_HEADERS` overrides the CLI's own headers.
function listingHeaders(
providerHeaders: Record<string, string>,
protocolHeaders: Record<string, string>,
): Headers {
const headers = new Headers({
authorization: `Bearer ${PROVIDER_AUTH_TOKEN_PLACEHOLDER}`,
accept: "application/json",
...protocolHeaders,
});
for (const [name, value] of Object.entries(providerHeaders)) {
// `Headers.set` puts an invalid value into its error message, so name only the header.
try {
headers.set(name, value);
} catch {
throw new GatewayListingError(`Model listing cannot send the invalid header "${name}"`);
}
}
return headers;
}
function stringField(entries: unknown[], field: string): string[] {
return entries.flatMap((entry) => {
const value = (entry as Record<string, unknown> | null)?.[field];
return typeof value === "string" && value.trim() !== "" ? [value] : [];
});
}
/**
* Reads `endpoint` page by page; `parse` returns `undefined` for a body it does
* not understand. A 4xx answer on any page means the gateway has no listing:
* the result is `undefined`, and nothing is narrowed.
*/
async function listPages(
endpoint: string,
firstQuery: Record<string, string>,
headers: Headers,
logger: ListingLogger,
parse: (body: unknown) => ListingPage | undefined,
): Promise<string[] | undefined> {
const { pathname: path, username, password } = new URL(endpoint);
const failure = (reason: string): GatewayListingError => {
const error = new GatewayListingError(`Model listing GET ${path} ${reason}`);
logger.error(`[providers/set] ${error.message}`);
return error;
};
// fetch refuses such a URL and repeats it, credentials included, in its error.
if (username || password) throw failure("cannot be sent: the base URL carries credentials");
const signal = AbortSignal.timeout(LISTING_TIMEOUT_MS);
const ids: string[] = [];
let query: Record<string, string> | undefined = firstQuery;
for (let page = 0; page < MAX_LISTING_PAGES && query; page++) {
const url = new URL(endpoint);
for (const [name, value] of Object.entries(query)) url.searchParams.set(name, value);
let response: Response;
let body: string;
try {
response = await fetch(url, { headers, signal });
body = await response.text();
} catch (error) {
throw failure(`failed: ${error instanceof Error ? error.message : String(error)}`);
}
if (response.status >= 400 && response.status < 500) {
logger.log(
`[providers/set] Model listing GET ${path} returned ${response.status}: the gateway has no listing, models are not narrowed`,
);
return undefined;
}
if (!response.ok) {
throw failure(`returned ${response.status}: ${body.slice(0, MAX_LOGGED_BODY_CHARS)}`);
}
let parsed: ListingPage | undefined;
try {
parsed = parse(JSON.parse(body));
} catch {
parsed = undefined;
}
if (!parsed) throw failure("returned an unexpected body");
ids.push(...parsed.ids);
query = parsed.nextQuery;
}
if (query) {
logger.error(
`[providers/set] Model listing GET ${path} still had more pages after ${MAX_LISTING_PAGES}; using what was read`,
);
}
return ids;
}

View File

@@ -1,4 +1,4 @@
import { RequestError } from "@agentclientprotocol/sdk";
import { RequestError } from "@agentclientprotocol/sdk/unstable";
import type { SDKActiveGoalMessage } from "@anthropic-ai/claude-agent-sdk";
export const GOAL_EXTENSION_VERSION = 1 as const;

View File

@@ -32,7 +32,7 @@
* until the next prompt. Awaiting it would add a CLI run to every turn.
*/
import { RequestError } from "@agentclientprotocol/sdk";
import { RequestError } from "@agentclientprotocol/sdk/unstable";
import type { AccountInfo, Query } from "@anthropic-ai/claude-agent-sdk";
export function shouldHideClaudeAuth(): boolean {

View File

@@ -2,7 +2,7 @@
import { claudeCliPath } from "./acp-agent.js";
import { applyManagedPolicyEnv } from "./managed-policy.js";
import { serveAcp } from "./serve.js";
import { acpProtocolsFromEnv, serveAcp } from "./serve.js";
import packageJson from "../package.json" with { type: "json" };
import { appendFileSync, mkdirSync } from "node:fs";
import { join } from "node:path";
@@ -83,9 +83,8 @@ if (process.argv.includes("--cli")) {
: undefined;
logger?.log("Claude ACP started");
// The draft ACP v2 is opt-in while it is a draft.
const acp = serveAcp(logger, {
experimentalV2: process.env.CLAUDE_AGENT_ACP_EXPERIMENTAL_V2 === "1",
protocols: acpProtocolsFromEnv(process.env.CLAUDE_AGENT_ACP_PROTOCOL),
});
async function shutdown() {

View File

@@ -1,4 +1,4 @@
import type { AvailableCommand } from "@agentclientprotocol/sdk";
import type { AvailableCommand } from "@agentclientprotocol/sdk/unstable";
import type { McpServerStatus, Query } from "@anthropic-ai/claude-agent-sdk";
import { escapeMarkdown } from "./usage-markdown.js";
import { raceTimeoutAndAbort } from "./utils.js";

View File

@@ -1,9 +1,10 @@
import { RequestError, type SessionNotification } from "@agentclientprotocol/sdk";
import { RequestError, type SessionNotification } from "@agentclientprotocol/sdk/unstable";
import type { AcpSessionNotification, SubagentForm, SubagentState } from "./acp-subagents.js";
import { AIR_SUBAGENT_KEY, airExtensionMeta } from "./air-extension.js";
import { isFinalToolCallStatus } from "./tool-calls/renderer.js";
/** The work state of a child, as the RFD's `subagent_update.state` carries it. */
type WorkState = NonNullable<
export type WorkState = NonNullable<
Extract<SessionNotification["update"], { sessionUpdate: "subagent_update" }>["state"]
>;
@@ -37,10 +38,15 @@ export type NativeSubagent = {
missingError?: boolean;
/** The RFD form: the open permission and question requests of the child. */
openRequests?: number;
/** The RFD form: how many times the client cancelled the child (see
* `NativeSubagentRuntime.cancelChild`). */
cancels?: number;
/** The RFD form: the ids of the prompt messages in the child's transcript. */
promptIds?: Set<string>;
/** The RFD form: the calls that delegated to the child, whose runs ended. */
endedRuns?: Set<string>;
/** The RFD form: the calls that delegated to the child, whose runs handed back a report. */
handedBack?: Set<string>;
announced?: boolean;
terminalState?: SubagentState;
/** Connection-local single-flight state; never serialized on the wire. */
@@ -86,6 +92,31 @@ type SubagentIdentity = {
subagentType?: string;
};
/** An agent of the session's subagent history, as the runtime restores it. */
export type HistoricalSubagent = {
agentId: string;
/** The Agent or Task call that launched it. */
launchId: string;
/** The agent that made that call. None means the root session. */
parentAgentId?: string;
name?: string;
agentType?: string;
description?: string;
};
/**
* The agent `agentId` and its ancestors, root first, from the session's
* subagent history; empty when the history cannot place it (see
* `subagentLineage`).
*/
export type SubagentLineage = (agentId: string) => Promise<readonly HistoricalSubagent[]>;
/**
* The agent that the Agent or Task call `launchId` launched, from the session's
* subagent history; undefined when the history has no such call.
*/
export type LaunchedAgent = (launchId: string) => Promise<string | undefined>;
const MAX_PENDING_PARENTS = 64;
const MAX_PENDING_UPDATES = 256;
const MAX_PENDING_UPDATES_PER_PARENT = 32;
@@ -114,19 +145,54 @@ export class NativeSubagentRuntime {
*/
private readonly childByToolCall = new Map<string, NativeSubagent>();
private readonly taskFinishPromises = new Map<string, Promise<void>>();
/** The RFD form: the session that made each SendMessage call. */
/** The session that made each SendMessage call. */
private readonly senderByToolUse = new Map<string, string>();
/**
* The RFD form: the Agent/Task calls that the client is not shown, until
* their final update. Kept by id because the adapter's own report that
* closes an unfinished call has no `toolName` to recognize it by.
*/
private readonly hiddenLaunches = new Map<string, { background: boolean }>();
/**
* The RFD form: the SubagentHandback calls of announced children that the
* client is not shown yet, with their first report, the report from their
* input, and the call that delegated the run.
*/
private readonly handbacks = new Map<
string,
{ initial: AcpSessionNotification; message?: string; delegationId?: string }
>();
private readonly generationByTaskId = new Map<string, number>();
private readonly pending = new Map<string, AcpSessionNotification[]>();
private pendingCount = 0;
/** The AIR form: agents being restored from the history, by agent id. */
private readonly agentRestores = new Map<string, Promise<void>>();
/** The AIR form: launch ids being resolved to an agent, by launch id. */
private readonly launchRestores = new Map<string, Promise<void>>();
/** The AIR form: launch ids that the history has no restorable agent for. */
private readonly unresolvedLaunches = new Set<string>();
/** The AIR form: agents that the history could not place, so it is read once. */
private readonly unrestorableAgents = new Set<string>();
/** The latest generation number given to a restored agent. */
private lastRestoredGeneration = 0;
/** Bumped when the restore state is dropped; a restore that began before it is void. */
private restoreEpoch = 0;
/** The RFD form: how many times the parent was cancelled (see {@link awaitingUser}). */
private parentCancels = 0;
/** `form` is undefined for a client without native subagent sessions. */
/**
* `form` is undefined for a client without native subagent sessions. The RFD
* form restores an agent from before a load or resume with `lineage`. The AIR
* form does too, and `launchedAgent` finds the agent that a launch call made.
*/
constructor(
form: SubagentForm | undefined,
private readonly rootSessionId: string,
private readonly session: NativeSubagentSession,
private readonly publish: Publish,
private readonly logger: Logger,
private readonly lineage?: SubagentLineage,
private readonly launchedAgent?: LaunchedAgent,
) {
this.form = form;
this.enabled = form !== undefined;
@@ -146,7 +212,7 @@ export class NativeSubagentRuntime {
forcedSessionId?: string,
): Promise<AcpSessionNotification | null> {
const routed = await this.routeUpdate(notification, deliver, forcedSessionId);
if (routed && this.form === "rfd") this.rememberSendMessage(routed);
if (routed) this.rememberSendMessage(routed);
return routed;
}
@@ -158,7 +224,7 @@ export class NativeSubagentRuntime {
const { update } = notification;
const claudeMeta = update._meta?.claudeCode as
{ parentToolUseId?: string | null; toolName?: string } | undefined;
const isControl = isNativeSubagentControlUpdate(update);
const isControl = this.isControlUpdate(update);
if (!this.enabled) return notification;
@@ -205,7 +271,7 @@ export class NativeSubagentRuntime {
if (forcedSessionId) {
return { ...notification, sessionId: forcedSessionId };
}
return failedControlFallback(
return unexposedControlFallback(
initial,
notification,
parentSessionId ?? this.rootSessionId,
@@ -213,8 +279,19 @@ export class NativeSubagentRuntime {
}
return forcedSessionId ? { ...notification, sessionId: forcedSessionId } : null;
}
// The RFD form: the Agent/Task tool call is the parent's record of the
// delegation, so it stays a tool call of the session that made it.
// The RFD form: the parent's session messages replace the Agent/Task
// tool call, its prompt as the parent's outgoing message (see
// `linkedSessionMessages`) and its result as the child's reply (see
// `launchReply`). A call made inside a child that is not announced yet
// is buffered below, as any update of that child.
if (!parentPending) {
return this.routeRfdLaunch(
notification,
child,
parentSessionId ?? this.rootSessionId,
forcedSessionId,
);
}
}
const toolCallId =
@@ -233,12 +310,27 @@ export class NativeSubagentRuntime {
}
const owner = toolCallId ? this.childByToolCall.get(toolCallId) : undefined;
const announcedChild =
owner ??
(claudeMeta?.parentToolUseId
? this.childByParentToolUse.get(claudeMeta.parentToolUseId)
: undefined);
if (this.form === "rfd" && announcedChild?.announced) {
const handback = await this.routeRfdHandback(notification, announcedChild);
if (handback !== undefined) return handback;
}
if (owner) return this.toChild(owner, notification, toolCallId);
if (this.enabled && claudeMeta?.parentToolUseId) {
const child = this.childByParentToolUse.get(claudeMeta.parentToolUseId);
if (!child || !child.announced) {
this.buffer(claudeMeta.parentToolUseId, notification);
const launchId = claudeMeta.parentToolUseId;
this.buffer(launchId, notification);
// An id that this process never saw is the launch call of an agent
// from before a load or resume; a new Agent call is known by now.
if (!child && this.form === "air" && this.isUnseenLaunch(launchId)) {
await this.restoreLaunch(launchId, deliver);
}
return null;
}
return this.toChild(child, notification, toolCallId);
@@ -249,7 +341,11 @@ export class NativeSubagentRuntime {
async taskStarted(task: TaskStarted, deliver: Publish): Promise<void> {
if (!this.enabled) return;
const known = this.form === "rfd" ? this.children.get(task.taskId) : undefined;
const known =
this.form === "rfd"
? (this.children.get(task.taskId) ??
(await this.restore(task.taskId, task.toolUseId ?? undefined, deliver)))
: undefined;
if (known) {
// A child keeps its session for every delegation to it. The SDK starts
// a resumed agent again under the id of the call that resumed it.
@@ -262,6 +358,24 @@ export class NativeSubagentRuntime {
);
return;
}
// The SDK starts an agent from before a restart under the id of the
// SendMessage call that resumed it, as in the RFD form (see `restore`).
// Its output comes under the id of the call that launched it.
if (
this.form === "air" &&
task.subagentType &&
task.toolUseId &&
this.senderByToolUse.has(task.toolUseId) &&
!this.children.has(task.taskId)
) {
await this.restoreAgent(task.taskId, deliver, promptText(task.prompt));
const restored = this.children.get(task.taskId);
if (restored) {
this.taskByToolUse.set(task.toolUseId, task.taskId);
this.childByParentToolUse.set(task.toolUseId, restored);
return;
}
}
if (!task.subagentType) {
if (task.toolUseId) {
this.takePending(task.toolUseId);
@@ -318,11 +432,17 @@ export class NativeSubagentRuntime {
* In the RFD form, the running patch only reports that the child runs: its
* prompt is a guess (the latest SendMessage to the agent), and the
* `task_started` or the SendMessage result carries the delegation.
*
* In the AIR form, an agent that the runtime does not know is restored from
* the session's history (see {@link restoreAgent}).
*/
async taskResumed(taskId: string, deliver: Publish, prompt?: string): Promise<void> {
if (!this.enabled) return;
const previous = this.children.get(taskId);
if (!previous) return;
if (!previous) {
if (this.form === "air") await this.restoreAgent(taskId, deliver, prompt);
return;
}
const finishing = this.taskFinishPromises.get(taskId) ?? previous.terminalPromise;
if (finishing) await finishing.catch(() => {});
if (this.form === "rfd") {
@@ -358,7 +478,7 @@ export class NativeSubagentRuntime {
toolUseId?: string,
): Promise<void> {
if (this.form !== "rfd") return this.taskResumed(taskId, deliver, prompt);
const child = this.children.get(taskId);
const child = this.children.get(taskId) ?? (await this.restore(taskId, toolUseId, deliver));
if (!child) return;
const finishing = this.taskFinishPromises.get(taskId);
if (finishing) await finishing.catch(() => {});
@@ -476,6 +596,7 @@ export class NativeSubagentRuntime {
this.identityByToolUse.clear();
this.controlByToolUse.clear();
this.parentByToolUse.clear();
this.forgetRestores();
}
if (errors.length === 1) throw errors[0];
if (errors.length > 1) throw new AggregateError(errors, "Failed to finish native subagents");
@@ -488,20 +609,59 @@ export class NativeSubagentRuntime {
* foreground children that the turn waits on and, currently, background
* children too, and the SDK's task events report each as cancelled. A child
* that keeps running keeps its open requests
* (agentclientprotocol/agent-client-protocol#2308).
* (agentclientprotocol/agent-client-protocol#2308). The RFD form only counts
* the cancel, for {@link awaitingUser}.
*/
async parentCancelled(deliver: Publish): Promise<void> {
if (this.form === "air") await this.finishAll("cancelled", deliver);
else this.parentCancels++;
}
/**
* The RFD form: the client sent `session/cancel` for the child session
* `sessionId`, which the child's `cancel` capability permits (see
* `announceNativeSubagent`). `stop` stops the child's task, as
* `Query.stopTask` does: Claude Code stops the child's current work and the
* foreground subagents that it waits on, and the parent's work goes on. The
* SDK's task events report each stopped child `idle` with `cancelled` (see
* {@link finishTask}). Accepting the stop is no evidence that the child
* stopped, so nothing is reported here, and a child whose work already ended
* keeps its outcome. Returns false when `sessionId` is not a child session
* of this runtime. AIR's draft grants no `cancel`.
*/
async cancelChild(sessionId: string, stop: (taskId: string) => Promise<void>): Promise<boolean> {
if (this.form !== "rfd") return false;
for (const [taskId, child] of this.children) {
if (child.sessionId !== sessionId || !child.announced) continue;
child.cancels = (child.cancels ?? 0) + 1;
await stop(taskId);
return true;
}
return false;
}
/**
* Runs `request`, a permission or question request of the session
* `sessionId`. In the RFD form, a child waiting on it is `requires_action`
* until its last open request ends.
* until its last open request ends, and then `running` again.
*
* `signal` aborts when Claude Code withdraws the request, which is not an
* answer. If the root session, the child, or an ancestor of the child was
* cancelled while the request was open, Claude Code withdrew it as it
* stopped the child, and the SDK's events report the child `idle` next. The
* end of the request then reports no `running`, which would show a child
* that is being stopped as working. Without a cancel, something else
* answered the request in the user's place, such as a `PermissionRequest`
* hook, and the child runs on, so the end reports `running`.
*/
async awaitingUser<T>(sessionId: string, request: () => Promise<T>): Promise<T> {
async awaitingUser<T>(
sessionId: string,
request: () => Promise<T>,
signal?: AbortSignal,
): Promise<T> {
const child = this.form === "rfd" ? this.childOfSession(sessionId) : undefined;
if (!child?.announced) return request();
const cancelsAtOpen = this.cancelsAbove(child);
child.openRequests = (child.openRequests ?? 0) + 1;
if (child.openRequests === 1 && child.workState === "running") {
await this.reportState(child, { state: "requires_action" }).catch((error) =>
@@ -512,7 +672,9 @@ export class NativeSubagentRuntime {
return await request();
} finally {
child.openRequests = (child.openRequests ?? 1) - 1;
if (child.openRequests === 0 && child.workState === "requires_action") {
// Withdrawn after a cancel that came while the request was open.
const stopped = signal?.aborted === true && this.cancelsAbove(child) !== cancelsAtOpen;
if (child.openRequests === 0 && child.workState === "requires_action" && !stopped) {
await this.reportState(child, { state: "running" }).catch((error) =>
this.logger.log(`Failed to report that subagent ${child.sessionId} runs: ${error}`),
);
@@ -524,6 +686,180 @@ export class NativeSubagentRuntime {
this.takePending(parentToolUseId);
}
/**
* The RFD form: restores the agent `agentId` when the SendMessage call
* `toolUseId` resumes it and the runtime does not know it, because it ran
* before the session was loaded or resumed. A new launch is no resume: its
* call is an Agent or Task call.
*
* The agent keeps its session id from before, its agent id. The session's
* subagent history (see {@link SubagentLineage}) gives the call that
* launched it and its ancestors. The launch call is its delegation again:
* Claude Code sends the resumed run's messages under that call's id. Before the agent's traffic, the RFD requires its complete
* ancestor chain, parent before child. So each ancestor that the client was
* not told about in this connection is announced, with no work state since
* nothing says that it runs; then the agent, as running. The RFD forbids
* guessing a parent, so an agent that the history cannot place stays
* unexposed, as before.
*/
private async restore(
agentId: string,
toolUseId: string | undefined,
deliver: Publish,
): Promise<NativeSubagent | undefined> {
if (!this.lineage || !toolUseId || !this.senderByToolUse.has(toolUseId)) return undefined;
const lineage = await this.lineage(agentId).catch((error: unknown) => {
this.logger.log(
`Session ${this.rootSessionId}: failed to read the history of subagent ${agentId}: ${error}`,
);
return [];
});
for (const entry of lineage) {
let child = this.children.get(entry.agentId);
if (!child) {
child = {
sessionId: entry.agentId,
parentSessionId: entry.parentAgentId
? (this.children.get(entry.parentAgentId)?.sessionId ?? entry.parentAgentId)
: this.rootSessionId,
parentToolUseId: entry.launchId,
name: subagentDisplayName(entry.name, entry.description, entry.agentType, entry.agentId),
task: subagentDescription(undefined, entry.description),
...rfdLabels(entry.name, entry.agentType, entry.description),
};
this.children.set(entry.agentId, child);
this.taskByToolUse.set(entry.launchId, entry.agentId);
this.childByParentToolUse.set(entry.launchId, child);
}
if (child.announced) continue;
if (entry.agentId === agentId) {
await this.announce(child);
} else {
await this.publish({
sessionId: child.parentSessionId,
update: {
sessionUpdate: "subagent_update",
sessionId: child.sessionId,
...(child.title ? { title: child.title } : {}),
...(child.description ? { description: child.description } : {}),
},
});
child.announced = true;
}
for (const pending of this.takePending(entry.launchId)) await deliver(pending);
}
return this.children.get(agentId);
}
/**
* The AIR form: whether no call of this process made `launchId`. A new Agent
* call has a control update before its `task_started`, and a launch that was
* restored or looked up is registered or remembered.
*/
private isUnseenLaunch(launchId: string): boolean {
return (
!!this.launchedAgent && !this.unresolvedLaunches.has(launchId) && this.isUnseenCall(launchId)
);
}
/** Whether this runtime has no record of the tool call `toolUseId`. */
private isUnseenCall(toolUseId: string): boolean {
return (
!this.taskByToolUse.has(toolUseId) &&
!this.childByParentToolUse.has(toolUseId) &&
!this.controlByToolUse.has(toolUseId) &&
!this.parentByToolUse.has(toolUseId) &&
!this.identityByToolUse.has(toolUseId)
);
}
/**
* The AIR form: restores the agent that the call `launchId` launched, once.
* Concurrent callers wait for the same lookup. Without an agent, the updates
* buffered under `launchId` stay buffered and `launchId` is not looked up again.
*/
private restoreLaunch(launchId: string, deliver: Publish): Promise<void> {
const existing = this.launchRestores.get(launchId);
if (existing) return existing;
const epoch = this.restoreEpoch;
const restoring = (async () => {
const agentId = await this.launchedAgent?.(launchId).catch((error: unknown) => {
this.logger.log(
`Session ${this.rootSessionId}: failed to read the history of launch ${launchId}: ${error}`,
);
return undefined;
});
if (epoch !== this.restoreEpoch) return;
if (agentId) await this.restoreAgent(agentId, deliver);
if (epoch !== this.restoreEpoch) return;
const child = this.childByParentToolUse.get(launchId);
if (!child) {
this.unresolvedLaunches.add(launchId);
return;
}
// Updates that arrived while the generation was announced.
if (child.announced) for (const pending of this.takePending(launchId)) await deliver(pending);
})().finally(() => {
if (this.launchRestores.get(launchId) === restoring) this.launchRestores.delete(launchId);
});
this.launchRestores.set(launchId, restoring);
return restoring;
}
/**
* The AIR form: opens a generation for the agent `agentId` that the runtime
* does not know, because it ran before the session was loaded or resumed.
* The session's subagent history (see {@link SubagentLineage}) gives the
* call that launched it: Claude Code sends the resumed run's messages under
* that call's id, so it is the generation's parent tool use, and the updates
* buffered under it are delivered. The parent is the agent that made the call
* if it is live, else the root. An agent that the history cannot place is
* not restored. Concurrent signals for one agent open one generation.
*/
private restoreAgent(agentId: string, deliver: Publish, prompt?: string): Promise<void> {
if (!this.lineage || this.children.has(agentId) || this.unrestorableAgents.has(agentId)) {
return Promise.resolve();
}
const existing = this.agentRestores.get(agentId);
if (existing) return existing;
const lineage = this.lineage;
const epoch = this.restoreEpoch;
const restoring = (async () => {
const entries = await lineage(agentId).catch((error: unknown) => {
this.logger.log(
`Session ${this.rootSessionId}: failed to read the history of subagent ${agentId}: ${error}`,
);
return [];
});
if (epoch !== this.restoreEpoch || this.children.has(agentId)) return;
const entry = entries.at(-1);
if (!entry || entry.agentId !== agentId) {
this.unrestorableAgents.add(agentId);
return;
}
const parent = entry.parentAgentId ? this.children.get(entry.parentAgentId) : undefined;
await this.openGeneration(
agentId,
undefined,
{
parentSessionId:
parent && this.isLiveSession(parent.sessionId) ? parent.sessionId : this.rootSessionId,
parentToolUseId: entry.launchId,
name: subagentDisplayName(entry.name, entry.description, entry.agentType, agentId),
task: subagentDescription(undefined, entry.description),
...promptField(promptText(prompt)),
},
true,
deliver,
true,
);
})().finally(() => {
if (this.agentRestores.get(agentId) === restoring) this.agentRestores.delete(agentId);
});
this.agentRestores.set(agentId, restoring);
return restoring;
}
/**
* The RFD form: the child runs again, and keeps its session. `toolUseId` is
* the call that delegated, usually a SendMessage: the SDK runs the resumed
@@ -675,6 +1011,109 @@ export class NativeSubagentRuntime {
}
}
/**
* The RFD form: an update of the Agent/Task call that launches `child`,
* which the parent's session messages replace. Only a launch whose child
* was never exposed stays a tool call of the parent, with its final status,
* since the RFD lets the Agent represent unexposed work through the parent.
* A background launch completes before its child starts, so its completion
* is not such a case.
*/
private async routeRfdLaunch(
notification: AcpSessionNotification,
child: NativeSubagent | undefined,
parentSessionId: string,
forcedSessionId: string | undefined,
): Promise<AcpSessionNotification | null> {
const { update } = notification;
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
return null;
}
const toolCallId = update.toolCallId;
// A permission request already showed the call to the client, so it
// keeps getting its updates.
if (forcedSessionId) return { ...notification, sessionId: forcedSessionId };
const launch = this.hiddenLaunches.get(toolCallId) ?? { background: false };
if (isBackgroundLaunch(update.rawInput)) launch.background = true;
if (!isFinalToolCallStatus(update.status)) {
this.hiddenLaunches.set(toolCallId, launch);
return null;
}
this.hiddenLaunches.delete(toolCallId);
if (child?.announcePromise) await child.announcePromise;
const background = launch.background;
if (child?.announced) {
const reply =
update.status === "completed" && !background && !child.handedBack?.has(toolCallId)
? launchReply(toolCallId, child.sessionId, child.parentSessionId, update)
: undefined;
if (reply) await this.publish(reply);
return null;
}
if (background && update.status === "completed") return null;
this.takePending(toolCallId);
const initial = this.controlByToolUse.get(toolCallId);
this.cleanupControl(toolCallId);
return unexposedControlFallback(initial, notification, parentSessionId);
}
/**
* The RFD form: an update of a SubagentHandback call of the announced
* `child`, which its report replaces: the child's message to its parent,
* see {@link linkedSessionMessages}. A failed call delivered nothing, so the
* child is shown the call after all. `undefined` for any other update.
*/
private async routeRfdHandback(
notification: AcpSessionNotification,
child: NativeSubagent,
): Promise<AcpSessionNotification | null | undefined> {
const { update } = notification;
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
return undefined;
}
const claudeMeta = update._meta?.claudeCode as
{ parentToolUseId?: string | null; toolName?: string } | undefined;
let handback = this.handbacks.get(update.toolCallId);
if (!handback) {
if (claudeMeta?.toolName !== HANDBACK_TOOL) return undefined;
handback = {
initial: notification,
delegationId: claudeMeta.parentToolUseId ?? child.parentToolUseId,
};
this.handbacks.set(update.toolCallId, handback);
}
handback.message = handbackMessage(update.rawInput) ?? handback.message;
if (!isFinalToolCallStatus(update.status)) return null;
this.handbacks.delete(update.toolCallId);
if (update.status === "completed" && handback.message) {
for (const view of linkedSessionMessages(
update.toolCallId,
child.sessionId,
child.parentSessionId,
handback.message,
)) {
await this.publish(view);
}
if (handback.delegationId) (child.handedBack ??= new Set()).add(handback.delegationId);
return null;
}
return unexposedControlFallback(handback.initial, notification, child.sessionId);
}
/** Whether `update` is of an Agent/Task call, by its tool name or, in the
* RFD form, by the id of a call the client is not shown. */
private isControlUpdate(
update: AcpSessionNotification["update"],
): update is Extract<
AcpSessionNotification["update"],
{ sessionUpdate: "tool_call" | "tool_call_update" }
> {
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
return false;
}
return this.hiddenLaunches.has(update.toolCallId) || isNativeSubagentControlUpdate(update);
}
private rememberToolCallOwner(toolCallId: string, child: NativeSubagent): void {
this.childByToolCall.delete(toolCallId);
this.childByToolCall.set(toolCallId, child);
@@ -684,6 +1123,24 @@ export class NativeSubagentRuntime {
}
}
/**
* The RFD form: how many cancels the client sent for `child`, its
* ancestors, and the root session. Each cancel stops the foreground work
* under it, and the counts only grow, so a changed sum shows that a cancel
* that reaches the child came.
*/
private cancelsAbove(child: NativeSubagent): number {
let cancels = this.parentCancels;
const seen = new Set<NativeSubagent>();
let next: NativeSubagent | undefined = child;
while (next && !seen.has(next)) {
seen.add(next);
cancels += next.cancels ?? 0;
next = this.childOfSession(next.parentSessionId);
}
return cancels;
}
/** The child generation with the ACP session `sessionId`, if one exists. */
private childOfSession(sessionId: string): NativeSubagent | undefined {
if (sessionId === this.rootSessionId) return undefined;
@@ -703,9 +1160,21 @@ export class NativeSubagentRuntime {
this.childByParentToolUse.clear();
this.taskFinishPromises.clear();
this.senderByToolUse.clear();
this.hiddenLaunches.clear();
this.handbacks.clear();
this.generationByTaskId.clear();
this.pending.clear();
this.pendingCount = 0;
this.forgetRestores();
}
/** Lets a later resume signal restore an agent again, and look up a launch again. */
private forgetRestores(): void {
this.agentRestores.clear();
this.launchRestores.clear();
this.unresolvedLaunches.clear();
this.unrestorableAgents.clear();
this.restoreEpoch++;
}
private takePending(parentToolUseId: string): AcpSessionNotification[] {
@@ -776,9 +1245,10 @@ export class NativeSubagentRuntime {
>,
announce: boolean,
deliver: Publish,
restored = false,
): Promise<void> {
const child: NativeSubagent = {
sessionId: this.nextChildSessionId(taskId, previous),
sessionId: this.nextChildSessionId(taskId, previous, restored),
...fields,
};
const toolUseId = child.parentToolUseId;
@@ -802,8 +1272,25 @@ export class NativeSubagentRuntime {
* `subagents/` history, so a fork cannot resume an agent of the original.
* If a fork ever can, the RFD requires remapping the copied ids, for
* example by deriving the child id from the root session id.
*
* An agent restored from the history (`restored`) is not on its first
* generation: before the restart the client already stored ids such as
* `taskId` and `taskId:generation:2`. Its generation is the current time in
* milliseconds, so it is `taskId:generation:1760000000000`, and each later
* resume in this process adds one.
*/
private nextChildSessionId(taskId: string, previous: NativeSubagent | undefined): string {
private nextChildSessionId(
taskId: string,
previous: NativeSubagent | undefined,
restored = false,
): string {
if (restored) {
// A clear() and a new restore within one millisecond must not repeat an id.
const generation = Math.max(Date.now(), this.lastRestoredGeneration + 1);
this.lastRestoredGeneration = generation;
this.generationByTaskId.set(taskId, generation);
return `${taskId}:generation:${generation}`;
}
if (!previous) {
this.generationByTaskId.set(taskId, 1);
return taskId;
@@ -817,8 +1304,10 @@ export class NativeSubagentRuntime {
/**
* Announces the child on its parent, once, before any update of the child.
* The RFD form announces it as running and then sends the prompt as the
* first message of the child. The child gets no capabilities, so the client
* may not cancel or otherwise change it.
* first message of the child. It grants the child `cancel`: the client may
* stop the child's current work with `session/cancel` (see
* `NativeSubagentRuntime.cancelChild`), and nothing else. AIR's draft grants
* no capabilities.
*/
export async function announceNativeSubagent(
child: NativeSubagent,
@@ -837,6 +1326,7 @@ export async function announceNativeSubagent(
sessionId: child.sessionId,
...(child.title ? { title: child.title } : {}),
...(child.description ? { description: child.description } : {}),
capabilities: { cancel: {} },
state: { state: "running" },
},
});
@@ -849,6 +1339,7 @@ export async function announceNativeSubagent(
child.parentToolUseId,
child.parentSessionId,
publish,
{ replacesLaunch: true },
);
}
return;
@@ -909,6 +1400,10 @@ export async function finishNativeSubagent(
* Agent/Task call or a SendMessage, so a prompt whose id is already in the
* transcript is the same delegation and is not sent again. `sender` is the
* session that made that call, when known.
*
* A launch's prompt (`replacesLaunch`) also goes to the parent, see
* {@link linkedSessionMessages}. A SendMessage call stays a tool call of the
* sender, so its message has only the incoming view.
*/
async function publishPrompt(
child: NativeSubagent,
@@ -916,11 +1411,18 @@ async function publishPrompt(
toolUseId: string | undefined,
sender: string | undefined,
publish: Publish,
{ replacesLaunch = false }: { replacesLaunch?: boolean } = {},
): Promise<void> {
child.promptIds ??= new Set();
const messageId = toolUseId ?? `${child.sessionId}:prompt:${child.promptIds.size + 1}`;
if (child.promptIds.has(messageId)) return;
child.promptIds.add(messageId);
if (replacesLaunch && sender !== undefined && toolUseId !== undefined) {
for (const view of linkedSessionMessages(toolUseId, sender, child.sessionId, prompt)) {
await publish(view);
}
return;
}
await publish({
sessionId: child.sessionId,
update: {
@@ -933,6 +1435,114 @@ async function publishPrompt(
});
}
/**
* The RFD form: a message that a tool call sent, which the client is not
* shown, as two session messages: the sender's outgoing view, then the
* recipient's incoming one. That call is a launch's Agent/Task call, whose
* prompt goes from the parent to the child, or a child's SubagentHandback
* call, whose report goes from the child to the parent. Both views use the
* call's tool use id, and each names the other with the draft RFD's
* `recipientMessageId` and `senderMessageId`.
*/
export function linkedSessionMessages(
toolUseId: string,
senderSessionId: string,
recipientSessionId: string,
text: string,
): AcpSessionNotification[] {
const message = {
sessionUpdate: "session_message" as const,
messageId: toolUseId,
senderSessionId,
recipientSessionId,
content: [{ type: "text" as const, text }],
};
return [
{
sessionId: senderSessionId,
update: { ...message, recipientMessageId: toolUseId } as AcpSessionNotification["update"],
},
{
sessionId: recipientSessionId,
update: { ...message, senderMessageId: toolUseId } as AcpSessionNotification["update"],
},
];
}
/**
* The tool with which a child hands its report to its parent, when Claude
* Code runs in hand-back mode. The Agent/Task result then only says that the
* report was delivered as a message, with `handback: "send"` in its
* structured result.
*/
export const HANDBACK_TOOL = "SubagentHandback";
/** The report of a SubagentHandback call, from its input. */
export function handbackMessage(input: unknown): string | undefined {
const message = (input as { message?: unknown } | null | undefined)?.message;
return typeof message === "string" && message.trim() ? message : undefined;
}
/**
* The RFD form: the answer of a foreground child, from the completed result
* of the Agent/Task call `launchId` that launched it, as the child's message
* in the parent's transcript (its incoming view). It replaces that call's
* result, which the parent is not shown. The child's transcript has its
* answer as its own agent message, not as a session message, so this view
* links to no other. `undefined` for a result without text. Not used for a
* run whose child handed back its report: the result has no report then.
*/
export function launchReply(
launchId: string,
childSessionId: string,
parentSessionId: string,
result: Extract<
AcpSessionNotification["update"],
{ sessionUpdate: "tool_call_update" | "tool_call" }
>,
): AcpSessionNotification | undefined {
const content = replyContent(result);
if (content.length === 0) return undefined;
return {
sessionId: parentSessionId,
update: {
sessionUpdate: "session_message",
messageId: `${launchId}:reply`,
senderSessionId: childSessionId,
recipientSessionId: parentSessionId,
content,
},
};
}
/**
* The text of a completed launch's result, from the tool call's `content`:
* `AgentReporter` removed the parts meant for the model there (the
* `agentId: … (use SendMessage …)` line, the `<usage>` block and the hand-back
* frame), which `rawOutput` still has.
*/
function replyContent(
result: Extract<
AcpSessionNotification["update"],
{ sessionUpdate: "tool_call_update" | "tool_call" }
>,
): Array<{ type: "text"; text: string }> {
return (result.content ?? []).flatMap((item) =>
item.type === "content" && item.content.type === "text" && item.content.text.trim()
? [{ type: "text" as const, text: item.content.text }]
: [],
);
}
/** Whether an Agent/Task call's input runs its child in the background. */
function isBackgroundLaunch(rawInput: unknown): boolean {
return (
typeof rawInput === "object" &&
rawInput !== null &&
(rawInput as { run_in_background?: unknown }).run_in_background === true
);
}
/** The RFD form: whether the child's foreground work is in progress. */
function isWorking(child: NativeSubagent): boolean {
return child.workState === "running" || child.workState === "requires_action";
@@ -950,7 +1560,7 @@ function isWorking(child: NativeSubagent): boolean {
* stream ended, is idle with no stop reason: its work ended, but the adapter
* did not see a cancellation or a failure, which the RFD forbids claiming.
*/
function rfdEndState(state: SubagentState, end: TaskEnd | undefined): WorkState {
export function rfdEndState(state: SubagentState, end: TaskEnd | undefined): WorkState {
if (end?.unexplained || end?.reason === "worker_restart") return { state: "idle" };
switch (state) {
case "completed":
@@ -976,7 +1586,7 @@ function rfdEndState(state: SubagentState, end: TaskEnd | undefined): WorkState
* name, with its role after it when both are known, else the role. The
* description is the task's short description, never the prompt.
*/
function rfdLabels(
export function rfdLabels(
name: unknown,
role: unknown,
description: unknown,
@@ -1096,7 +1706,13 @@ function isFailedToolCallUpdate(update: AcpSessionNotification["update"]): boole
);
}
function failedControlFallback(
/**
* A tool call that the client was not shown, as one `tool_call` with its
* final status, shown after all: an Agent/Task call whose child was never
* exposed (AIR's draft shows only a failed one, the RFD form also a completed
* one), or a failed SubagentHandback call.
*/
function unexposedControlFallback(
initial: AcpSessionNotification | undefined,
terminal: AcpSessionNotification,
sessionId: string,
@@ -1115,7 +1731,7 @@ function failedControlFallback(
update: {
...terminal.update,
sessionUpdate: "tool_call",
status: "failed",
status: terminal.update.status ?? "failed",
// The synthesized tool_call is this call's first report, so give it
// the standard `name` the initial one would have carried.
...(typeof claudeMeta?.toolName === "string" ? { name: claudeMeta.toolName } : {}),
@@ -1136,7 +1752,7 @@ function failedControlFallback(
...initial.update,
...terminal.update,
sessionUpdate: "tool_call",
status: "failed",
status: terminal.update.status ?? "failed",
title:
typeof terminal.update.title === "string" && terminal.update.title.trim().length > 0
? terminal.update.title

View File

@@ -1,4 +1,4 @@
import type { RequestPermissionResponse } from "@agentclientprotocol/sdk";
import type { RequestPermissionResponse } from "@agentclientprotocol/sdk/unstable";
import type {
PermissionMode,
PermissionResult,

View File

@@ -1,4 +1,4 @@
import type { PermissionOption } from "@agentclientprotocol/sdk";
import type { PermissionOption } from "@agentclientprotocol/sdk/unstable";
import {
buildEditPermissionOptions,
buildGlobPermissionOptions,

View File

@@ -1,4 +1,4 @@
import type { PermissionOption } from "@agentclientprotocol/sdk";
import type { PermissionOption } from "@agentclientprotocol/sdk/unstable";
import os from "node:os";
import path from "node:path";
import type { DurablePermissionChangeSet } from "../normalization.js";

View File

@@ -1,4 +1,4 @@
import type { PermissionOption } from "@agentclientprotocol/sdk";
import type { PermissionOption } from "@agentclientprotocol/sdk/unstable";
import type { DurablePermissionChangeSet } from "../normalization.js";
export const PERMISSION_OPTION_ID = {
@@ -30,6 +30,9 @@ export interface PermissionOptionContext {
/** The mode the session left when it entered plan mode. */
prePlanMode?: string;
contextUsedPercent?: number;
/** False when no ACP turn can continue an accepted plan in a fresh
* context. Then an ExitPlanMode request offers no clear-context option. */
allowContextReset?: boolean;
}
export function allowOnce(name = "Yes"): PermissionOption {

View File

@@ -1,4 +1,4 @@
import type { PermissionOption } from "@agentclientprotocol/sdk";
import type { PermissionOption } from "@agentclientprotocol/sdk/unstable";
import path from "node:path";
import type { DurablePermissionChangeSet } from "../normalization.js";
import { plainString, type PermissionOptionContext, withOptionalUpdate } from "./shared.js";

View File

@@ -1,4 +1,4 @@
import type { PermissionOption } from "@agentclientprotocol/sdk";
import type { PermissionOption } from "@agentclientprotocol/sdk/unstable";
import {
allowOnce,
exactLocalAllowRule,
@@ -70,7 +70,7 @@ export function buildExitPlanModePermissionOptions(
: (["auto", "bypassPermissions"] as const)
).filter((mode) => modes.has(mode));
const preferredMode = elevatedModes[0] ?? "acceptEdits";
if (plainString(context.input.plan)) {
if (context.allowContextReset !== false && plainString(context.input.plan)) {
const usage =
context.contextUsedPercent === undefined ? "" : ` (${context.contextUsedPercent}% used)`;
options.push(

View File

@@ -1,4 +1,4 @@
import type { RequestPermissionRequest, ToolCallContent } from "@agentclientprotocol/sdk";
import type { RequestPermissionRequest, ToolCallContent } from "@agentclientprotocol/sdk/unstable";
import { AIR_PERMISSION_KEY, withAirMeta } from "../air-extension.js";
import { ClientCapabilities } from "../tool-calls/client-capabilities.js";
import { AcpToolCallRenderer } from "../tool-calls/renderer.js";

View File

@@ -1,5 +1,5 @@
import type { RequestPermissionResponse } from "@agentclientprotocol/sdk";
import type { PermissionOption } from "@agentclientprotocol/sdk";
import type { RequestPermissionResponse } from "@agentclientprotocol/sdk/unstable";
import type { PermissionOption } from "@agentclientprotocol/sdk/unstable";
import type { PermissionMode, PermissionResult } from "@anthropic-ai/claude-agent-sdk";
import type { DurablePermissionChangeSet } from "./normalization.js";
import { applyClaudePermissionSelection, parseClaudePermissionSelection } from "./effects.js";

44
src/prompt-suggestions.ts Normal file
View File

@@ -0,0 +1,44 @@
/**
* The follow-up prompt the SDK predicts for the next turn.
*
* With `promptSuggestions` on, the CLI emits one `prompt_suggestion` message
* after a turn's `result` holding the prompt it expects the user to send next.
* ACP has no field for it, so it travels as
* `session_info_update._meta.promptSuggestions`: a client that does not read
* the key sees a session info update that says nothing and ignores it.
*
* AIR renders the first non-blank `suggestions[].text` as gray text in an empty
* composer, where Tab fills the composer without sending.
*
* The CLI sends no suggestion on the first turn of a session, after an API
* error, in plan mode, and at the account's usage limit, so most sessions have
* turns that end with no prediction at all.
*/
/** The `_meta` key the suggestion travels under. It names no vendor. */
export const PROMPT_SUGGESTIONS_META_KEY = "promptSuggestions";
/** One predicted prompt, as the `_meta` value carries it. */
export type PromptSuggestionsMetaValue = {
suggestions: { text: string }[];
};
/**
* The suggestion as it goes on the wire, or `null` for a suggestion that says
* nothing: the CLI can predict whitespace, and an empty text would otherwise
* reach the composer as a hint with no content.
*/
export function promptSuggestionText(raw: string | null | undefined): string | null {
const text = raw?.trim();
return text ? text : null;
}
/**
* The `_meta` of the session info update that publishes `text`. `null` sends an
* empty `suggestions` list, which clears the prompt the client holds — the turn
* predicted nothing, so the composer must not keep the previous prediction.
*/
export function promptSuggestionsMeta(text: string | null): Record<string, unknown> {
const value: PromptSuggestionsMetaValue = { suggestions: text === null ? [] : [{ text }] };
return { [PROMPT_SUGGESTIONS_META_KEY]: value };
}

View File

@@ -1,19 +1,32 @@
import { ndJsonStream } from "@agentclientprotocol/sdk";
import * as v2 from "@agentclientprotocol/sdk/experimental/v2";
import { v1AgentApp, type ClaudeAcpAgent, type Logger } from "./acp-agent.js";
import { nodeToWebReadable, nodeToWebWritable } from "./utils.js";
import { v2AgentApp } from "./v2/agent.js";
/**
* Serves ACP on stdio.
*
* With `experimentalV2`, the connection goes through {@link acpProtocolRouter}.
* Without it, the adapter serves ACP v1 alone and never routes by protocol
* version, exactly as before.
* The ACP versions that the adapter serves: `v1` or `v2` alone, or both with
* `auto`, where the client's `initialize` picks the version.
*/
export type AcpProtocols = "auto" | "v1" | "v2";
/**
* Reads `CLAUDE_AGENT_ACP_PROTOCOL`: unset, empty, or `auto` serves both
* versions, and `v1` or `v2` serves that version alone. Throws on any other
* value, so that a typo such as `CLAUDE_AGENT_ACP_PROTOCOL=1` fails at startup
* instead of silently serving both.
*/
export function acpProtocolsFromEnv(value: string | undefined): AcpProtocols {
if (value === undefined || value === "") return "auto";
if (value === "auto" || value === "v1" || value === "v2") return value;
throw new Error(
`CLAUDE_AGENT_ACP_PROTOCOL must be "auto", "v1", or "v2", not ${JSON.stringify(value)}`,
);
}
/** Serves ACP on stdio through {@link acpProtocolRouter}. */
export function serveAcp(
logger: Logger | undefined,
options: { experimentalV2: boolean },
options: { protocols: AcpProtocols },
): { closed: Promise<void>; dispose(): Promise<void> } {
const output = nodeToWebWritable(process.stdout);
const input = nodeToWebReadable(process.stdin);
@@ -23,9 +36,9 @@ export function serveAcp(
const onAgent = (created: ClaudeAcpAgent) => {
agent = created;
};
const { closed } = options.experimentalV2
? acpProtocolRouter(logger, onAgent).connect(v2.ndJsonStream(output, input))
: v1AgentApp(logger, onAgent).connect(ndJsonStream(output, input));
const { closed } = acpProtocolRouter(logger, onAgent, options.protocols).connect(
v2.ndJsonStream(output, input),
);
return {
closed,
dispose: async () => {
@@ -36,16 +49,19 @@ export function serveAcp(
/**
* Routes a connection to the protocol version that the client negotiates in
* `initialize`: the experimental draft ACP v2 for a client that requests
* version 2 or later, and ACP v1 otherwise. `onAgent` receives the agent of
* the connection once the router has opened it.
* `initialize`. With `auto`, a client that requests version 2 or later gets
* the draft ACP v2, and every other client gets ACP v1. With `v1`, a v2
* client is negotiated down to v1. With `v2`, the router rejects the
* `initialize` of a v1 client as an unsupported protocol version. `onAgent`
* receives the agent of the connection once the router has opened it.
*/
export function acpProtocolRouter(
logger: Logger | undefined,
onAgent: (agent: ClaudeAcpAgent) => void,
protocols: AcpProtocols = "auto",
): v2.AgentProtocolRouter {
return v2
.agentProtocolRouter()
.withV1(v1AgentApp(logger, onAgent))
.withV2(v2AgentApp(logger, onAgent));
const router = v2.agentProtocolRouter();
if (protocols !== "v2") router.withV1(v1AgentApp(logger, onAgent));
if (protocols !== "v1") router.withV2(v2AgentApp(logger, onAgent));
return router;
}

View File

@@ -1,4 +1,4 @@
import type { SessionConfigOption } from "@agentclientprotocol/sdk";
import type { SessionConfigOption } from "@agentclientprotocol/sdk/unstable";
import type { EffortLevel, ModelInfo, Settings } from "@anthropic-ai/claude-agent-sdk";
import { AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY, withAirMeta } from "./air-extension.js";
import { EFFORT_CONFIG_ID } from "./session-config-ids.js";

View File

@@ -1,4 +1,4 @@
import type { ClientCapabilities, SessionNotification } from "@agentclientprotocol/sdk";
import type { ClientCapabilities, SessionNotification } from "@agentclientprotocol/sdk/unstable";
import {
getSessionMessages,
type SDKAssistantMessageError,

View File

@@ -11,6 +11,10 @@
* - an interactive CLI that is not driven by an SDK holds it, and its registry
* status is newer than the transcript: the registry status (an idle one is
* `error` after such a turn);
* - a turn that ended with an open background task is `running`, also after
* 10 minutes: Claude Code wakes the session again when the task ends. The
* wait ends when the registry status is `idle`, because a task can end
* without a transcript record (a stop in the task UI);
* - otherwise the transcript tail: a finished turn is `idle` (`error` after
* an API error), an unfinished turn written in the last 10 minutes is
* `running`, and anything else has no known state.
@@ -72,6 +76,14 @@ function usesRegistryStatus(record: LiveRecord, transcriptMtimeMs: number): bool
);
}
/** Whether the last turn ended while a background task of the session is
* open. */
function waitsForBackgroundTasks(facts: TranscriptFacts): boolean {
if (facts.turnState !== "finished") return false;
const openTasks = facts.backgroundTasks?.open ?? [];
return openTasks.length > 0;
}
function iso(ms: number | undefined): string | undefined {
return ms === undefined || !Number.isFinite(ms) ? undefined : new Date(ms).toISOString();
}
@@ -103,6 +115,9 @@ export function deriveActivity(input: {
// A busy or waiting CLI wins; an idle one shows how its turn ended.
state = registryState(live.status);
if (state === "idle" && failed) state = "error";
} else if (waitsForBackgroundTasks(facts) && live.status !== "idle") {
// Claude Code wakes the session again when an open task ends.
state = "running";
} else if (failed) {
state = "error";
} else if (facts.turnState === "finished") {

View File

@@ -0,0 +1,223 @@
/**
* The background tasks of a session that did not end yet, from its transcript.
*
* A Claude Code session can start a background shell command or a background
* sub-agent and end its turn at once. When the task ends, Claude Code wakes
* the session with a `<task-notification>` input for a short turn. So a turn
* end with an open task is no end of the work: the session list keeps such a
* session `running` (see `activity.ts`).
*
* The records, as Claude Code 2.1.299 writes them, in the rules of AIR's
* native Claude provider:
*
* - a tool result opens a task when its `toolUseResult` has a
* `backgroundTaskId` (a shell command), or `isAsync: true` with an
* `agentId` (a sub-agent);
* - a task closes when its notification reaches the session: as a user
* record, or as a `queue-operation` `remove` when a running turn takes it
* in. A notification that waits in the queue (`enqueue`) keeps the task
* open, because the session still runs a turn for it;
* - only a notification with a `<status>` closes a task. A notification
* without one comes while the task still runs: the check-in of a quiet
* shell ("is still running after …", since Claude Code 2.1.303) or a
* `Monitor` event;
* - a `TaskStop` result closes the task of its `toolUseResult.task_id`:
* Claude Code writes no notification for it. The result does not name its
* tool, so its `tool_use_id` must match a `TaskStop` call. A `task_id` in
* the result of another tool keeps the task open;
* - a resumed CLI writes a `stopped` notification for each task of the
* previous process;
* - a sidechain record belongs to a sub-agent and opens or closes no task of
* the session.
*
* A background shell that does not end, for example a dev server, keeps its
* task open, so its session stays `running` while the CLI holds it.
*
* A task that a sub-agent starts is not seen. Its start record is only in the
* transcript of the sub-agent, so the session can be `idle` while the
* sub-agent waits for it.
*/
import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";
import { TASK_NOTIFICATION_TAG, taskNotificationsOf } from "../task-notifications.js";
export type BackgroundTasks = {
/** The ids of the tasks that started and did not end, in start order. */
open: string[];
/** The `tool_use` ids of the `TaskStop` calls without a result. */
stopCalls: string[];
/** Whether the sets hold the records of the whole file. A set from the
* tail window only does not know a task that started before it. */
complete: boolean;
};
/** The fields of a transcript record that bear on a task. */
type TranscriptRecord = {
type?: string;
isSidechain?: boolean;
/** Of a `queue-operation` record: `enqueue`, `remove`, ... */
operation?: string;
/** Of a `queue-operation` record: the queued input. */
content?: string;
message?: { content?: string | ContentBlock[] };
toolUseResult?: ToolUseResult;
};
type ContentBlock = {
type?: string;
text?: string;
/** Of a `tool_use` block. */
id?: string;
name?: string;
/** Of a `tool_result` block. */
tool_use_id?: string;
};
type ToolUseResult = {
/** The task of a background shell command. */
backgroundTaskId?: string;
/** With `agentId`: an async sub-agent. */
isAsync?: boolean;
agentId?: string;
/** The task that a `TaskStop` call stopped. Another tool can give one. */
task_id?: string;
};
const NOTIFICATION_TAG = `<${TASK_NOTIFICATION_TAG}>`;
/** Text that a line must hold to bear on a task, so that the other lines
* are not parsed. */
const TASK_MARKERS = ["backgroundTaskId", '"isAsync"', NOTIFICATION_TAG, "TaskStop", "task_id"];
function parseRecord(line: string): TranscriptRecord | undefined {
try {
return JSON.parse(line) as TranscriptRecord;
} catch {
return undefined;
}
}
function blocksOf(record: TranscriptRecord): ContentBlock[] {
const content = record.message?.content;
return Array.isArray(content) ? content : [];
}
/** The input of a user record: its string content, else its first text
* block. */
function inputTextOf(record: TranscriptRecord): string | undefined {
const content = record.message?.content;
if (typeof content === "string") return content;
return blocksOf(record).find((block) => block.type === "text")?.text;
}
/** The task that a `<task-notification>` input ends: only an input that
* starts with the tag, after whitespace, as AIR's `TaskNotification` reads
* it. Only a notification with a `<status>` ends a task (see
* `taskNotificationsOf`). */
export function endedTaskId(input: string | undefined): string | undefined {
if (!input?.trimStart().startsWith(NOTIFICATION_TAG)) return undefined;
return taskNotificationsOf(input)[0]?.task_id;
}
/** The task that a tool result started: a background shell command, or an
* async sub-agent. */
function startedTaskId(record: TranscriptRecord): string | undefined {
if (record.type !== "user") return undefined;
const result = record.toolUseResult;
if (result?.backgroundTaskId) return result.backgroundTaskId;
if (result?.isAsync) return result.agentId;
return undefined;
}
/** The task whose notification reached the session: as a user record, or
* as a queue `remove` when a running turn takes it in. */
function deliveredTaskId(record: TranscriptRecord): string | undefined {
if (record.type === "user") return endedTaskId(inputTextOf(record));
if (record.type === "queue-operation" && record.operation === "remove") {
return endedTaskId(record.content);
}
return undefined;
}
/** Collects the tasks of the lines it is given, in file order. */
class TaskTracker {
private readonly open: Set<string>;
private readonly stopCalls: Set<string>;
constructor(after?: BackgroundTasks) {
this.open = new Set(after?.open);
this.stopCalls = new Set(after?.stopCalls);
}
add(line: string): void {
const mayBearOnTask = TASK_MARKERS.some((marker) => line.includes(marker));
if (!mayBearOnTask) return;
const record = parseRecord(line);
// A sidechain record belongs to a sub-agent, which receives the
// notifications of its own tasks.
if (!record || record.isSidechain) return;
this.apply(record);
}
result({ complete }: { complete: boolean }): BackgroundTasks {
return { open: [...this.open], stopCalls: [...this.stopCalls], complete };
}
private apply(record: TranscriptRecord): void {
const started = startedTaskId(record);
if (started) this.open.add(started);
const delivered = deliveredTaskId(record);
if (delivered) this.open.delete(delivered);
this.applyTaskStop(record);
}
/** A `task_id` stops its task only in the result of a `TaskStop` call:
* another tool can give a `task_id` too. */
private applyTaskStop(record: TranscriptRecord): void {
let isTaskStopResult = false;
for (const block of blocksOf(record)) {
if (block.type === "tool_use" && block.name === "TaskStop" && block.id) {
this.stopCalls.add(block.id);
}
const resultId = block.tool_use_id;
if (resultId && this.stopCalls.has(resultId)) {
this.stopCalls.delete(resultId);
isTaskStopResult = true;
}
}
const stopped = record.toolUseResult?.task_id;
if (isTaskStopResult && stopped) this.open.delete(stopped);
}
}
/**
* The tasks of the lines of `text`, applied on the tasks `after`.
*
* `after` can be what an earlier scan found for a shorter copy of the file,
* and `text` can repeat records of that copy. The result is still the same:
* the last record that opens or closes a task decides, and `text` holds the
* last records.
*/
export function backgroundTasksOf(
text: string,
options: { complete: boolean; after?: BackgroundTasks },
): BackgroundTasks {
const tracker = new TaskTracker(options.after);
for (const line of text.split("\n")) tracker.add(line);
return tracker.result({ complete: options.complete });
}
/** The tasks of the first `size` bytes of `filePath`, from all its records.
* Reads the whole file, so only for a session that a live process holds. */
export async function searchBackgroundTasks(
filePath: string,
size: number,
): Promise<BackgroundTasks> {
const tracker = new TaskTracker();
if (size === 0) return tracker.result({ complete: true });
const stream = createReadStream(filePath, { start: 0, end: size - 1, encoding: "utf8" });
const lines = createInterface({ input: stream, crlfDelay: Infinity });
for await (const line of lines) tracker.add(line);
return tracker.result({ complete: true });
}

View File

@@ -15,8 +15,7 @@ import {
type InitializeRequest,
type ListSessionsRequest,
type ListSessionsResponse,
} from "@agentclientprotocol/sdk";
import { deleteSession as sdkDeleteSession } from "@anthropic-ai/claude-agent-sdk";
} from "@agentclientprotocol/sdk/unstable";
import type { Query } from "@anthropic-ai/claude-agent-sdk";
import type { AcpClient, Logger, Session } from "../acp-agent.js";
import {
@@ -24,6 +23,7 @@ import {
AIR_SESSION_INDEX_CAPABILITY,
AIR_SESSION_LIST_SUBSCRIBE_CAPABILITY,
AIR_SESSION_RENAME_CAPABILITY,
AIR_SESSION_WORKTREES_CAPABILITY,
clientSupportsAirCapability,
withAirMeta,
} from "../air-extension.js";
@@ -41,6 +41,7 @@ import {
} from "./list-subscriptions.js";
import {
archiveInsteadOfDelete,
deleteSessionIfStored,
archiveTo,
renameTo,
parseRenameSessionRequest,
@@ -143,8 +144,8 @@ export class SessionIndexConnection {
}
/** The AIR capabilities the agent advertises for the index: the index
* itself, archive, rename and the list subscription, all only to a client
* that declared `sessionIndex`. */
* itself, archive, rename, the list subscription and the `agentWorktree`
* row field, all only to a client that declared `sessionIndex`. */
capabilities(): string[] {
return this.service
? [
@@ -152,6 +153,7 @@ export class SessionIndexConnection {
AIR_SESSION_ARCHIVE_CAPABILITY,
AIR_SESSION_RENAME_CAPABILITY,
AIR_SESSION_LIST_SUBSCRIBE_CAPABILITY,
AIR_SESSION_WORKTREES_CAPABILITY,
]
: [];
}
@@ -187,6 +189,31 @@ export class SessionIndexConnection {
this.service?.ownSessionChanged(sessionId);
}
/**
* Runs `fork`, the handler of a `session/fork` request of `sourceId`. The
* session list holds back the forks of the source until the response is
* out. The client then knows a fork from the response before a list shows
* it.
*/
async forking<T>(sourceId: string, fork: () => Promise<T>): Promise<T> {
// The forks of this connection: the request handler reads the service
// once, so a later `initialize` does not split the start and the answer.
const service = this.service;
service?.forkStarted(sourceId);
try {
return await fork();
} finally {
// The SDK writes the response in the microtasks after the handler
// resolves, so a `setTimeout(0)` callback runs after it.
setTimeout(() => service?.forkAnswered(sourceId), 0);
}
}
/** The `session/fork` of `sourceId` copied the transcript into `forkId`. */
onForkCreated(sourceId: string, forkId: string): void {
this.service?.forkCreated(sourceId, forkId);
}
/** `_session/list/subscribe`. */
subscribeList(params: ListSubscribeRequest): Promise<ListSubscribeResponse> {
return this.requireService(LIST_SUBSCRIBE_METHOD).subscribeList(params.cwd);
@@ -336,7 +363,8 @@ export class SessionIndexConnection {
* - Another AIR client uses delete to mark a session done, and may reopen
* it later: the adapter archives it instead (as `_session/archive` does,
* after the CLI closed), so the transcript survives.
* - Every other client: the SDK delete, as before.
* - Every other client: the SDK delete. A session without a transcript
* succeeds (see {@link deleteSessionIfStored}).
*/
async deleteSession(params: DeleteSessionRequest): Promise<DeleteSessionResponse> {
const loaded = this.host.agent.sessions[params.sessionId] !== undefined;
@@ -350,7 +378,7 @@ export class SessionIndexConnection {
} else if (this.host.isAirClient()) {
await archiveInsteadOfDelete(params.sessionId, this.airArchiver());
} else {
await sdkDeleteSession(params.sessionId);
await deleteSessionIfStored(params.sessionId);
}
return {};
}

View File

@@ -40,7 +40,7 @@ import { randomUUID } from "node:crypto";
import * as fs from "node:fs";
import * as fsp from "node:fs/promises";
import * as path from "node:path";
import { RequestError, type SessionInfo } from "@agentclientprotocol/sdk";
import { RequestError, type SessionInfo } from "@agentclientprotocol/sdk/unstable";
import type { OwnSessionState } from "./activity.js";
import {
bySession,
@@ -111,6 +111,8 @@ export type ListSubscriptionDeps = {
/** What this connection knows of a session it runs. */
own: (sessionId: string) => OwnSessionState | undefined;
notify: (changes: ListChanges) => Promise<void>;
/** The forks whose rows wait for their fork response. */
forkHolds?: ForkHolds;
logError: (message: string, error: unknown) => void;
now?: () => number;
debounceMs?: number;
@@ -242,6 +244,54 @@ function transcriptId(filename: string): string | undefined {
return isSessionId(id) ? id : undefined;
}
/**
* The `session/fork` requests in flight. A client must get the id of a fork
* from the fork response before a list shows the fork. Otherwise the client
* sees an unknown session and lists it as a session of its own.
*/
export class ForkHolds {
/** Source session id → the number of its forks in flight, and the forks
* they copied the transcript into. */
private readonly sources = new Map<string, { count: number; forks: Set<string> }>();
started(sourceId: string): void {
const source = this.sources.get(sourceId);
if (source) source.count++;
else this.sources.set(sourceId, { count: 1, forks: new Set() });
}
/** A fork of `sourceId` copied the transcript into `forkId`. */
created(sourceId: string, forkId: string): void {
this.sources.get(sourceId)?.forks.add(forkId);
}
/** The response of a fork of `sourceId` is out. Returns whether that
* freed rows: the last fork of the source answered. */
answered(sourceId: string): boolean {
const source = this.sources.get(sourceId);
if (!source) return false;
if (--source.count > 0) return false;
this.sources.delete(sourceId);
return true;
}
/** Whether `id` is a fork whose fork response is not out yet. */
isHeld(id: string): boolean {
for (const { forks } of this.sources.values()) if (forks.has(id)) return true;
return false;
}
/**
* Whether the row of `id` waits. A fork that a fork in flight created
* waits. Before the copy returns its id, an unsent row of a fork of a
* source in flight waits too, by its `forkedFrom`.
*/
holds(id: string, sent: boolean, forkedFrom: string | undefined): boolean {
if (this.isHeld(id)) return true;
return !sent && forkedFrom !== undefined && this.sources.has(forkedFrom);
}
}
export class ListSubscriptions {
private readonly subscriptions = new Map<string, Subscription>();
/** Sessions whose row shows this connection's SDK state: the rescan
@@ -348,6 +398,13 @@ export class ListSubscriptions {
}
}
/** Sends the pending sessions now: {@link ForkHolds} held some back. */
deliverPending(): void {
for (const subscription of this.subscriptions.values()) {
if (subscription.pending.size > 0) this.deliver(subscription);
}
}
/**
* This connection wrote the transcripts of a session: a rename, archive,
* unarchive or delete. Its transcripts are stat'ed and its row read again
@@ -1199,7 +1256,10 @@ export class ListSubscriptions {
if (!watch.rows.has(id)) candidates.push(...(priorFiles.get(id) ?? []));
}
if (candidates.length === 0) return new Set();
const { rows, unknown } = await this.deps.index.cachedRowsOf(paths, candidates);
// Only the ids count: no worktree check.
const { rows, unknown } = await this.deps.index.cachedRowsOf(paths, candidates, {
mark: false,
});
for (const row of rows) unknown.add(row.sessionId);
return unknown;
}
@@ -1224,6 +1284,12 @@ export class ListSubscriptions {
subscription.pending.delete(id);
continue;
}
// A fork whose fork response is not out yet stays pending.
if (
this.deps.forkHolds?.holds(id, subscription.sent.has(id), current?.row.facts.forkedFrom)
) {
continue;
}
const dueAt = (subscription.sentAt.get(id) ?? -Infinity) + this.minIntervalMs;
if (dueAt > now) {
nextAt = Math.min(nextAt, dueAt);

View File

@@ -15,7 +15,7 @@ import {
RequestError,
type ListSessionsRequest,
type ListSessionsResponse,
} from "@agentclientprotocol/sdk";
} from "@agentclientprotocol/sdk/unstable";
import { deleteSession as sdkDeleteSession } from "@anthropic-ai/claude-agent-sdk";
import { airExtensionMeta } from "../air-extension.js";
import { sanitizeTitle } from "../session-titles.js";
@@ -28,6 +28,7 @@ import {
visibleTitle,
} from "./archive-title.js";
import {
ForkHolds,
ListSubscriptions,
type ListChanges,
type ListSubscribeResponse,
@@ -383,6 +384,28 @@ function sdkDeleteError(sessionId: string): Error {
);
}
/**
* `session/delete` of a client that is neither AIR nor a `sessionIndex` client:
* the SDK delete of the session's transcript. A session without one succeeds,
* as ACP asks ("Deleting an already-deleted session, or a session that never
* existed, SHOULD succeed silently"): an id that is not a session id, a
* session deleted before, or a session this connection ran but never prompted,
* since Claude Code writes the transcript at the first prompt. Other failures
* still fail the request.
*/
export async function deleteSessionIfStored(
sessionId: string,
deleteSession: (sessionId: string) => Promise<void> = sdkDeleteSession,
): Promise<void> {
if (!isSessionId(sessionId)) return;
try {
await deleteSession(sessionId);
} catch (error) {
if (isSdkNotFound(error)) return;
throw error;
}
}
/**
* `session/delete` of an AIR client without `sessionIndex`, which uses delete
* to mark a session done: archives it instead, so the transcript survives. A
@@ -442,6 +465,9 @@ export class SessionIndexService {
readonly index: SessionIndex;
readonly registry: LiveSessionRegistry;
private subscriptions?: ListSubscriptions;
/** Here and not in the subscriptions, which start with the first subscribe:
* a subscribe during a fork holds that fork back too. */
private readonly forkHolds = new ForkHolds();
/** Set for good by {@link dispose}: no subscription is made after it. */
private disposed = false;
/** The mutation in flight per session, which the next one waits for. */
@@ -450,12 +476,18 @@ export class SessionIndexService {
private readonly deleteSession: (sessionId: string) => Promise<void>;
constructor(private readonly deps: SessionIndexDeps) {
this.index = new SessionIndex();
this.registry = deps.registry ?? new LiveSessionRegistry();
this.index = new SessionIndex({ isLive: (sessionId) => this.isLive(sessionId) });
this.now = deps.now ?? Date.now;
this.deleteSession = deps.deleteSession ?? ((id) => sdkDeleteSession(id));
}
/** Whether a live process holds the session, by the live registry. */
private async isLive(sessionId: string): Promise<boolean> {
const live = await this.registry.snapshot();
return live.has(sessionId);
}
/** `session/list` of a `sessionIndex` client. `own` reports the sessions
* that this connection runs. */
async list(
@@ -480,9 +512,10 @@ export class SessionIndexService {
});
const live = await livePromise;
const now = this.now();
const sessions = rows.map((row) =>
sessionInfoOf(row, own(row.sessionId), live.get(row.sessionId), now),
);
const sessions = rows
// A fork whose fork response is not out yet is left out, as in the list subscriptions.
.filter((row) => !this.forkHolds.isHeld(row.sessionId))
.map((row) => sessionInfoOf(row, own(row.sessionId), live.get(row.sessionId), now));
const last = rows[rows.length - 1];
return hasMore && last ? { sessions, nextCursor: encodeListCursor(last, scope) } : { sessions };
}
@@ -686,6 +719,7 @@ export class SessionIndexService {
registry: this.registry,
own: (sessionId) => this.deps.ownSessionState?.(sessionId),
notify: (changes) => this.deps.notifyListChanges?.(changes) ?? Promise.resolve(),
forkHolds: this.forkHolds,
logError: this.deps.logError,
...this.deps.listSubscriptionTiming,
});
@@ -702,6 +736,22 @@ export class SessionIndexService {
this.subscriptions?.ownSessionChanged(sessionId);
}
/** A `session/fork` of `sourceId` started (see {@link ForkHolds}). */
forkStarted(sourceId: string): void {
this.forkHolds.started(sourceId);
}
/** The `session/fork` of `sourceId` copied the transcript into `forkId`. */
forkCreated(sourceId: string, forkId: string): void {
this.forkHolds.created(sourceId, forkId);
}
/** The response of a `session/fork` of `sourceId` is out: the list
* subscriptions send the rows they held back. */
forkAnswered(sourceId: string): void {
if (this.forkHolds.answered(sourceId)) this.subscriptions?.deliverPending();
}
dispose(): void {
this.disposed = true;
this.subscriptions?.dispose();

View File

@@ -52,8 +52,9 @@ import {
type SdkTitles,
type TranscriptFacts,
} from "./transcript-scan.js";
import { searchBackgroundTasks } from "./background-tasks.js";
import { DirListings, statFiles } from "./dir-listing.js";
import { worktreeCounterparts } from "./worktrees.js";
import { isInLinkedWorktree, worktreeCounterparts } from "./worktrees.js";
export const DEFAULT_LIST_LIMIT = 50;
export const MAX_LIST_LIMIT = 200;
@@ -125,6 +126,10 @@ export type IndexRow = {
facts: TranscriptFacts;
mtimeMs: number;
archived: boolean;
/** The session runs in a linked git worktree that Claude Code made for
* it: its transcript says so (`facts.agentWorktree`) and `cwd` lies in
* a linked worktree. See {@link markAgentWorktrees}. */
agentWorktree?: true;
};
/** A keyset position: the order key and the session id of the last row. */
@@ -249,6 +254,22 @@ function toRow({ candidate, metadata }: Resolved, cwd: string): IndexRow {
};
}
/**
* Sets `agentWorktree` on the rows whose transcript says Claude Code made
* their worktree and whose cwd lies in a linked worktree now. Only those rows
* are checked; the check is not cached, so a removed worktree shows at once.
*/
async function markAgentWorktrees(rows: IndexRow[]): Promise<IndexRow[]> {
await Promise.all(
rows.map(async (row) => {
if (row.facts.agentWorktree === true && (await isInLinkedWorktree(row.cwd))) {
row.agentWorktree = true;
}
}),
);
return rows;
}
function isAfter(row: { orderAtMs: number; sessionId: string }, cursor: ListCursor): boolean {
return compareRows(row, cursor) > 0;
}
@@ -380,7 +401,15 @@ function scopeOf(
};
}
export type SessionIndexOptions = {
/** Whether a live process holds the session. Only for such a session the
* whole transcript is searched for its background tasks. */
isLive?: (sessionId: string) => Promise<boolean>;
};
export class SessionIndex {
constructor(private readonly options: SessionIndexOptions = {}) {}
private readonly listings = new DirListings();
private readonly metadata = new Lru<string, CacheEntry>(METADATA_CACHE_SIZE);
/** The metadata reads in flight, by path: a list and a subscription that
@@ -686,11 +715,14 @@ export class SessionIndex {
* alone gives, read from it at once, and the ids of the sessions it gives
* no answer for: a transcript of it has nothing cached at its stat, no cwd
* known, or is continued in another session (whether the successor hides
* it depends on the successor now). Reads no transcript.
* it depends on the successor now). Reads no transcript. The rows carry
* `agentWorktree` ({@link markAgentWorktrees}, which reads git files)
* unless `mark` is false, for a caller that uses only their ids.
*/
async cachedRowsOf(
paths: readonly string[],
candidates: readonly TranscriptCandidate[],
{ mark = true }: { mark?: boolean } = {},
): Promise<{ rows: IndexRow[]; unknown: Set<string> }> {
const unknown = new Set<string>();
const resolved: { item: Resolved; cwd: string }[] = [];
@@ -721,7 +753,8 @@ export class SessionIndex {
if (previous && previous.mtimeMs >= item.candidate.mtimeMs) continue;
newest.set(id, { row: toRow(item, cwd), mtimeMs: item.candidate.mtimeMs });
}
return { rows: [...newest.values()].map(({ row }) => row), unknown };
const rows = [...newest.values()].map(({ row }) => row);
return { rows: mark ? await markAgentWorktrees(rows) : rows, unknown };
}
/**
@@ -831,7 +864,7 @@ export class SessionIndex {
);
await settlePending();
}
return rows();
return markAgentWorktrees(rows());
}
/** Whether the cached metadata of `candidate`, still current, places it
@@ -968,14 +1001,15 @@ export class SessionIndex {
this.reads++;
const headTail = await readHeadTail(candidate.filePath, candidate.size);
if (isSidechainTranscript(headTail.head)) return null;
const facts = await scanTranscriptFile(
const scanned = await scanTranscriptFile(
candidate.filePath,
candidate.size,
headTail,
previous,
candidate.ino,
);
if (!facts.hasMessages) return null;
if (!scanned.hasMessages) return null;
const facts = await this.withAllBackgroundTasks(candidate, scanned);
// The last relocation names the session's cwd, as the SDK reads it; the
// messages before it keep the old one.
const fileCwd = await this.recoverCwd(candidate.dirName, [
@@ -1008,6 +1042,24 @@ export class SessionIndex {
};
}
/**
* `facts` with the background tasks of the whole transcript, when the
* tasks of its tail are not complete: a task can start before the tail.
* Only a finished turn of a live session waits for the tasks, so only
* then the whole transcript is read.
*/
private async withAllBackgroundTasks(
candidate: TranscriptCandidate,
facts: TranscriptFacts,
): Promise<TranscriptFacts> {
if (facts.turnState !== "finished") return facts;
if (facts.backgroundTasks?.complete) return facts;
const isLive = await this.options.isLive?.(candidate.sessionId);
if (!isLive) return facts;
const backgroundTasks = await searchBackgroundTasks(candidate.filePath, candidate.size);
return { ...facts, backgroundTasks };
}
/**
* The titles the SDK gives the listed transcript (`customTitle` and
* `summary` of its `getSessionInfo`), by the SDK's rule from the head and

View File

@@ -4,7 +4,7 @@
* the RFDs in `_meta.jetbrains.air`.
*/
import type { SessionInfo } from "@agentclientprotocol/sdk";
import type { SessionInfo } from "@agentclientprotocol/sdk/unstable";
import { withAirMeta } from "../air-extension.js";
import { deriveActivity, selectCost, type OwnSessionState } from "./activity.js";
import type { LiveRecord } from "./live-registry.js";
@@ -38,6 +38,7 @@ export function sessionInfoOf(
lastPromptAt: iso(facts.lastPromptAt),
model: facts.model,
forkedFrom: facts.forkedFrom,
agentWorktree: row.agentWorktree,
state: activity?.state,
lastTurnEndedAt: activity?.lastTurnEndedAt,
cost: cost === undefined ? undefined : { amount: cost, currency: "USD" },

View File

@@ -7,9 +7,13 @@
*
* A last message longer than the tail window leaves no message in it: then
* the tail window grows, up to {@link MAX_TAIL_SIZE}, until it holds one.
*
* The open background tasks come from the tail, on top of the tasks of an
* earlier scan (see `background-tasks.ts`).
*/
import * as fs from "node:fs/promises";
import { backgroundTasksOf, type BackgroundTasks } from "./background-tasks.js";
import { firstPrompt, isUserPrompt, mediaPrompt, promptOf } from "./first-prompt.js";
const CHUNK_SIZE = 64 * 1024;
@@ -51,6 +55,14 @@ export type TranscriptFacts = {
/** The session this one was forked from: the `forkedFrom.sessionId` that
* the SDK and CLI fork write on every copied record. */
forkedFrom?: string;
/** What the last `worktree-state` record says: true when its
* `worktreeSession` is set and not `enteredExisting` (Claude Code made the
* worktree and the session is in it), false when it says otherwise.
* Undefined when no such record was read. See {@link agentWorktreeOf}. */
agentWorktree?: boolean;
/** The background tasks that did not end. From the tail, on top of what
* an earlier scan found when the file only grew within the tail. */
backgroundTasks?: BackgroundTasks;
/** Whether the head or the tail holds a user or an assistant message. A
* transcript without one is a metadata-only stub. */
hasMessages: boolean;
@@ -67,12 +79,12 @@ export type HeadTail = {
};
/** Facts that an earlier scan may have found before the tail window. */
const INHERITED_FACTS = ["model", "tailCwd", "lastTurnEndedAt"] as const;
const INHERITED_FACTS = ["model", "tailCwd", "lastTurnEndedAt", "agentWorktree"] as const;
/** What an earlier scan of the same file found, at `size`. */
export type PreviousScan = { size: number } & Pick<
TranscriptFacts,
"lastPromptAt" | "promptSearched" | (typeof INHERITED_FACTS)[number]
"lastPromptAt" | "promptSearched" | "backgroundTasks" | (typeof INHERITED_FACTS)[number]
>;
/** The last `window` bytes of a file of `size` bytes, without the first,
@@ -152,6 +164,14 @@ export async function scanTranscriptFile(
}
}
}
const previousTasks = grownWithinTail ? previous.backgroundTasks : undefined;
facts.backgroundTasks = tailBackgroundTasks(read.tail, size, previousTasks);
// No worktree record in the tail (or before it, by an earlier scan): the
// one `claude --worktree` writes at the start may still be in the head.
if (facts.agentWorktree === undefined) {
const agentWorktree = agentWorktreeOf(read.head);
if (agentWorktree !== undefined) facts.agentWorktree = agentWorktree;
}
if (facts.lastPromptAt !== undefined || size <= CHUNK_SIZE) {
facts.promptSearched = true;
} else if (grownWithinTail && (previous.lastPromptAt !== undefined || previous.promptSearched)) {
@@ -163,7 +183,8 @@ export async function scanTranscriptFile(
if (!facts.hasMessages || complete(facts) || size <= CHUNK_SIZE) return facts;
const key = `${filePath}\0${size}\0${identity ?? ""}`;
const searched = fullySearched.get(key);
if (searched) return searched;
// The tasks can differ by the earlier scan.
if (searched) return { ...searched, backgroundTasks: facts.backgroundTasks };
let result = facts;
const handle = await fs.open(filePath, "r");
try {
@@ -199,9 +220,15 @@ export async function scanTranscriptFile(
...wider,
...(facts.model !== undefined && { model: facts.model }),
...(facts.tailCwd !== undefined && { tailCwd: facts.tailCwd }),
// The wider tail holds the narrow one, so a record it has is the
// latest; else the narrow scan's (inherited or from the head) stands.
...((wider.agentWorktree ?? facts.agentWorktree) !== undefined && {
agentWorktree: wider.agentWorktree ?? facts.agentWorktree,
}),
// A turn end the wider tail lacks may lie before it: the
// narrow scan inherited it from an earlier scan of this file.
lastTurnEndedAt: wider.lastTurnEndedAt ?? facts.lastTurnEndedAt,
backgroundTasks: facts.backgroundTasks,
hasMessages: facts.hasMessages,
lastPromptAt,
// Searched to the start of the file, or found.
@@ -221,6 +248,22 @@ export async function scanTranscriptFile(
return result;
}
/**
* The background tasks of the tail of a file of `size` bytes. `previous`:
* the tasks of an earlier scan, when the file only grew within the tail
* since. The set is complete when the tail is the whole file, or when it
* holds all records after a complete earlier set.
*/
function tailBackgroundTasks(
tail: string,
size: number,
previous: BackgroundTasks | undefined,
): BackgroundTasks {
if (size <= CHUNK_SIZE) return backgroundTasksOf(tail, { complete: true });
if (previous?.complete) return backgroundTasksOf(tail, { complete: true, after: previous });
return backgroundTasksOf(tail, { complete: false });
}
const RELOCATED_MARKER = '"relocated"';
/** The cwd of the last `relocated` record of the tail: where the session
@@ -238,6 +281,43 @@ export function relocatedCwd(tail: string): string | undefined {
return undefined;
}
const WORKTREE_STATE_MARKER = '"worktree-state"';
/**
* Whether the last `worktree-state` record of `text` says the session is in
* a worktree that Claude Code made for it, as AIR's native Claude provider
* reads it: a `worktreeSession` that is an object without
* `enteredExisting: true`. The last line that parses as a `worktree-state`
* record decides; any other `worktreeSession` (`null`, which the CLI writes
* when the session leaves the worktree, missing, or no object) means false.
* Only lines that are not JSON (such as the cut first line of a window) are
* skipped. Undefined when `text` holds no such record.
*
* The CLI writes the record when it enters or leaves a worktree (for
* `claude --worktree`, at the start of the session, so in the head), and
* writes it again with the other session metadata at the end of the
* transcript (on exit and as the transcript grows), so the tail usually
* holds the latest one. Only the head and the tail are read: a session that
* entered a worktree midway through a long transcript, with no metadata
* written after it yet, is not seen until the CLI writes the metadata again.
*/
export function agentWorktreeOf(text: string): boolean | undefined {
if (!text.includes(WORKTREE_STATE_MARKER)) return undefined;
const lines = text.split("\n");
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i]!;
if (!line.includes(WORKTREE_STATE_MARKER)) continue;
const entry = parseLine(line);
// Only a line that is no JSON record is skipped: the latest record that
// parses decides, even when its `worktreeSession` is missing or no object.
if (entry?.type !== "worktree-state") continue;
const session = entry.worktreeSession;
if (!session || typeof session !== "object" || Array.isArray(session)) return false;
return (session as { enteredExisting?: unknown }).enteredExisting !== true;
}
return undefined;
}
/** The cwd a transcript belongs to, as the SDK reads it: the last
* `relocated` record of the tail, else the first cwd of the head. */
export function transcriptProjectCwd({ head, tail }: HeadTail): string | undefined {
@@ -344,6 +424,10 @@ export function scanTranscript(
if (headCwd !== undefined) facts.headCwd = decodeJsonString(headCwd);
const forkedFrom = FORKED_FROM_PATTERN.exec(head)?.[1];
if (forkedFrom !== undefined) facts.forkedFrom = forkedFrom;
// The tail only: scanTranscriptFile falls back to the head after it
// inherits what an earlier scan found before the tail.
const agentWorktree = agentWorktreeOf(tail);
if (agentWorktree !== undefined) facts.agentWorktree = agentWorktree;
const lines = tail.split("\n");
let turnEndFound = false;

View File

@@ -115,6 +115,18 @@ export async function worktreeCounterparts(cwd: string): Promise<string[]> {
return [...new Set(existing.filter((value): value is string => value !== undefined))];
}
/**
* Whether `cwd` lies in a linked worktree: its git dir is
* `<common-dir>/worktrees/<name>`. False in the main worktree, in a
* submodule and outside a repository.
*/
export async function isInLinkedWorktree(cwd: string): Promise<boolean> {
const gitDir = await findGitDir(cwd);
if (!gitDir) return false;
const commonDir = await commonDirOf(gitDir);
return path.resolve(path.dirname(gitDir)) === path.resolve(commonDir, "worktrees");
}
/**
* Every existing worktree of the repository that contains `cwd`: the main
* worktree and the linked ones. Empty outside a repository.

View File

@@ -5,7 +5,7 @@ import {
type SessionNotification,
type SetSessionModeRequest,
type SetSessionModeResponse,
} from "@agentclientprotocol/sdk";
} from "@agentclientprotocol/sdk/unstable";
import type {
ModelInfo,
PermissionMode,

View File

@@ -1,6 +1,7 @@
import type { SessionConfigOption } from "@agentclientprotocol/sdk";
import type { SessionConfigOption } from "@agentclientprotocol/sdk/unstable";
import type { ModelInfo, Query, Settings } from "@anthropic-ai/claude-agent-sdk";
import { AIR_RECOMMENDED_CONFIG_VALUE_CAPABILITY, withAirMeta } from "./air-extension.js";
import { bedrockRegionPrefix } from "./gateway-models.js";
export const MODEL_CONFIG_ID = "model";
@@ -262,31 +263,47 @@ function resolveSettingsModel(
const MODEL_FAMILY_ALIASES = new Set(["opus", "sonnet", "haiku", "fable"]);
/** A model id without the spellings `deniedModels` ignores: provider
* prefixes, dates, version suffixes, `-fast` and context hints. */
function normalizeDeniedModelId(value: string): string {
/** A model id without the spellings that name the same model: provider
* prefixes, dates, version suffixes, `:variant` suffixes, dotted versions,
* `-fast` and context hints. `deniedModels` and gateway listings both match on
* it. OpenRouter lists `anthropic/claude-opus-5.5:batch[1m]`, which normalizes
* to `claude-opus-5-5`; `anthropic/openai/gpt-5` keeps its `openai/` prefix and
* matches no Claude id. */
function normalizeModelId(value: string): string {
let id = stripContextHints(value.trim().toLowerCase());
const providerPrefix = id.lastIndexOf("anthropic.");
if (providerPrefix !== -1) id = id.slice(providerPrefix + "anthropic.".length);
return id
.replace(/@.*$/, "")
.replace(/-v\d+(?::\d+)?$/, "")
.replace(/-fast$/, "")
.replace(/-\d{8}$/, "");
return (
id
// Only a leading `anthropic/`: OpenRouter's `anthropic/openai/gpt-5` must
// keep `openai/`, which `lastIndexOf` would strip along with it.
.replace(/^anthropic\//, "")
// `:batch`, `:free`. A letter must follow the colon, so Bedrock's `-v1:0`
// is left for the `-vN:M` step below.
.replace(/:[a-z][a-z0-9-]*$/, "")
// `claude-opus-5.5` -> `claude-opus-5-5`; only dots between two digits.
.replace(/(?<=\d)\.(?=\d)/g, "-")
.replace(/@.*$/, "")
.replace(/-v\d+(?::\d+)?$/, "")
.replace(/-fast$/, "")
.replace(/-\d{8}$/, "")
);
}
/**
* Whether managed `deniedModels` blocks a model id, following the CLI's rules
* closely enough for the picker: a family alias blocks the whole family, and a
* model id blocks that version in every spelling plus the later minor versions
* it prefixes (`claude-opus-5` also blocks `claude-opus-5-5`). The CLI stays
* the enforcer; this only keeps the picker from offering a refused row.
* it prefixes (`claude-opus-5` also blocks `claude-opus-5-5`). Entries share
* `normalizeModelId`, so `claude-opus-4.5` or `anthropic/claude-opus-4.5` also
* blocks `claude-opus-4-5`, which the CLI may still allow. The CLI stays the
* enforcer; this only keeps the picker from offering a refused row.
*/
export function isDeniedModel(modelId: string, deniedModels: readonly string[]): boolean {
const id = normalizeDeniedModelId(modelId);
const id = normalizeModelId(modelId);
if (!id) return false;
return deniedModels.some((entry) => {
const denied = normalizeDeniedModelId(entry);
const denied = normalizeModelId(entry);
if (!denied) return false;
if (MODEL_FAMILY_ALIASES.has(denied)) return id === denied || id.includes(`-${denied}`);
return id === denied || id.startsWith(`${denied}-`);
@@ -379,6 +396,48 @@ export function applyAvailableModelsAllowlist(
return result;
}
/**
* Keep the SDK models the `providers/set` gateway listed (`listGatewayModels`,
* `listBedrockModels`). A row matches when its `resolvedModel` (or
* its `value` when the SDK gives none) names a listed model in any spelling:
* `claude-sonnet-5[1m]` matches a listed `claude-sonnet-5-20260101`, and
* `eu.anthropic.claude-sonnet-5` matches a listed `eu.anthropic.claude-sonnet-5-v1:0`.
*
* With a Bedrock `regionPrefix`, a row resolved to another cross-region prefix
* (`us.anthropic.claude-sonnet-5` under `eu`) is dropped even when the same
* model is listed under `eu`: the CLI would call it in the other region.
*
* A row the SDK gives no `resolvedModel` names its model only by an alias, so
* it is checked as far as the alias allows: `default` is kept, and a family
* alias such as `opus` is kept when the gateway lists a model of that family.
*
* When no row matches by its model (a `default` row kept only for lacking a
* `resolvedModel` does not count), the gateway serves none of the SDK's
* models, and nothing is left: the session offers no model.
*/
export function narrowToGatewayModels(
models: ModelInfo[],
listedModelIds: readonly string[],
regionPrefix?: string,
): ModelInfo[] {
const listed = new Set(listedModelIds.map(normalizeModelId).filter(Boolean));
const narrowed = models.filter((model) => {
const modelId = model.resolvedModel ?? model.value;
const prefix = bedrockRegionPrefix(modelId);
if (regionPrefix !== undefined && prefix !== undefined && prefix !== regionPrefix) {
return false;
}
const id = normalizeModelId(modelId);
if (listed.has(id)) return true;
if (model.resolvedModel !== undefined) return false;
if (model.value === "default") return true;
return MODEL_FAMILY_ALIASES.has(id) && [...listed].some((entry) => entry.includes(`-${id}`));
});
return narrowed.some((model) => listed.has(normalizeModelId(model.resolvedModel ?? model.value)))
? narrowed
: [];
}
/** Whether a rejected `setModel` was vetoed by a user-configured
* PreModelSwitch hook. The CLI's rejection message is the stable,
* distinguishable marker ("Model switch blocked by a PreModelSwitch hook:
@@ -397,7 +456,18 @@ export async function getAvailableModels(
isResumedSession: boolean,
sessionId: string,
resumedModelHint?: string,
narrowedByGateway = false,
): Promise<SessionModelState> {
// A gateway that lists none of the SDK's models leaves none to offer
// (`narrowToGatewayModels`). The session stays on the model the SDK runs,
// and the gateway refuses its requests.
if (models.length === 0) {
return {
availableModels: [],
currentModelId: (isResumedSession && resumedModelHint) || sdkModels[0]?.value || "default",
};
}
const settings = settingsManager.getSettings();
let currentModel = models[0];
@@ -444,9 +514,34 @@ export async function getAvailableModels(
// be re-asserted to keep the reported model truthful.
// Anything else (fuzzy match, allowlist-synthesized value, alias) gets a
// setModel call so we don't drift from the user's intended pin.
// Case (a) does not hold when `narrowToGatewayModels` left the SDK on a
// model the gateway does not list: the dropped `default` row of a fresh or
// transcript-less session, or a resumed transcript model outside the list.
// Such a session switches to models[0].
const sdkSawSameValue = sdkModels.some((m) => m.value === currentModel.value);
const sdkDefault = sdkModels.find((m) => m.value === "default");
const sdkDefaultDropped = sdkDefault !== undefined && !models.some((m) => m.value === "default");
let switchToListed = false;
if (resolvedFromInput === undefined) {
if (!isResumedSession || !resumedModelHint) {
switchToListed = sdkDefaultDropped;
} else if (
narrowedByGateway &&
(!models.some((m) => m.value === currentModel.value) ||
// A loose match can map the transcript's model onto a listed row of
// another version, which the CLI does not resume on.
(currentModel.resolvedModel !== undefined &&
normalizeModelId(currentModel.resolvedModel) !== normalizeModelId(resumedModelHint)))
) {
currentModel = models[0];
switchToListed = true;
}
}
// When the switch fails, the SDK stays on the model it ran before it.
const modelBeforeSwitch = (): ModelInfo =>
switchToListed && sdkDefault !== undefined ? sdkDefault : models[0];
const skipSetModel =
resolvedFromInput === undefined ||
(resolvedFromInput === undefined && !switchToListed) ||
(!isResumedSession && currentModel.value === resolvedFromInput && sdkSawSameValue);
if (!skipSetModel) {
const setModelStartedAt = performance.now();
@@ -478,15 +573,19 @@ export async function getAvailableModels(
// live model back on this path: getContextUsage isn't serviced on a
// fresh session until the first prompt turn (issues #886/#880).
if (!isResumedSession) {
if (!isPreModelSwitchHookBlock(error)) throw error;
// A switch onto a listed model is advisory too: the session stays on
// the model the SDK runs and reports it.
if (!switchToListed && !isPreModelSwitchHookBlock(error)) throw error;
logger.error(
`Model pin "${currentModel.value}" was vetoed by a PreModelSwitch hook; staying on the default model:`,
`Model switch to "${currentModel.value}" failed or was vetoed by a PreModelSwitch hook; staying on the default model:`,
error,
);
currentModel = models[0];
currentModel = modelBeforeSwitch();
} else {
logger.error(`Failed to re-assert model "${currentModel.value}" on resume:`, error);
currentModel = resumedModelHint ? matchResumedModel(models, resumedModelHint) : models[0];
currentModel = resumedModelHint
? matchResumedModel(models, resumedModelHint)
: modelBeforeSwitch();
}
}
}

View File

@@ -1,4 +1,4 @@
import type { ClientCapabilities, SessionNotification } from "@agentclientprotocol/sdk";
import type { ClientCapabilities, SessionNotification } from "@agentclientprotocol/sdk/unstable";
/** Fire-and-forget advisory text for the user: live events rather than
* conversation history. See the ACP Session Notices RFD

View File

@@ -1,20 +1,25 @@
/**
* AI-generated session titles.
*
* Claude Code's own auto-title generation never runs under the Agent SDK — the
* latch that arms it is pre-set in the headless path — so `SDKSessionInfo.summary`
* degrades to the raw first prompt. This module asks the CLI for a real title
* instead, via the `generate_session_title` control request, and publishes it
* over ACP as a `session_info_update`.
* Claude Code titles a new SDK session itself: when it reads the first prompt,
* it asks a small model for a title and stores it in the transcript as an
* `ai-title` entry, a few seconds in. It sends no message when it does. This
* module asks for the title with the `generate_session_title` control request,
* which returns the stored title or waits for the one being generated, and
* publishes it over ACP as a `session_info_update`. Where the CLI does not
* title the session itself (a resumed session, or one with
* `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` or
* `CLAUDE_CODE_DISABLE_TERMINAL_TITLE` set), the same request generates it.
*
* {@link SessionTitles} holds the per-session state and is owned by `Session`.
* `acp-agent.ts` drives it from four places: {@link SessionTitles.onPrompt} and
* `acp-agent.ts` drives it from five places: {@link SessionTitles.onPrompt} and
* {@link SessionTitles.onAssistantText} to collect text,
* {@link SessionTitles.onPromptQueued} once a prompt is sent to the CLI,
* {@link SessionTitles.onTurnEnd} at `session_state_changed: idle`, and
* {@link SessionTitles.reset} on `conversation_reset`.
*/
import type { ContentBlock, PromptRequest } from "@agentclientprotocol/sdk";
import type { ContentBlock, PromptRequest } from "@agentclientprotocol/sdk/unstable";
import { getSessionInfo, type Query, type SDKSessionInfo } from "@anthropic-ai/claude-agent-sdk";
import type { ClaudeAcpAgent, Session } from "./acp-agent.js";
import { ExplicitTitle } from "./session-index/explicit-title.js";
@@ -77,6 +82,11 @@ export function appendTitleContext(previous: string | undefined, text: string):
return next.length > MAX_TITLE_CONTEXT_LENGTH ? next.slice(-MAX_TITLE_CONTEXT_LENGTH) : next;
}
/** The text chunks of a prompt, one per line. */
function promptText(prompt: PromptRequest["prompt"]): string {
return prompt.flatMap((chunk) => (chunk.type === "text" ? [chunk.text] : [])).join("\n");
}
/** Whether this SDK still offers on-demand title generation. */
function supportsTitleGeneration(query: Query): boolean {
return typeof (query as TitleCapableQuery).generateSessionTitle === "function";
@@ -100,6 +110,11 @@ export class SessionTitles {
* one. Released by {@link reset}, and when generation yields nothing. */
private settled = false;
/** Whether {@link onPromptQueued} asked for a title from the latest prompt.
* A turn end then does not ask again: like the CLI, a prompt that yields
* no title gets one try, and the next prompt tries again. */
private askedAtPrompt = false;
/** The title a client set, which no generated title replaces. */
private readonly explicit = new ExplicitTitle();
@@ -132,11 +147,33 @@ export class SessionTitles {
if (this.settled) {
return;
}
const promptText = prompt
.flatMap((chunk) => (chunk.type === "text" ? [chunk.text] : []))
.join("\n");
this.context = appendTitleContext(this.context, `\n${promptText(prompt)}\n`);
}
this.context = appendTitleContext(this.context, `\n${promptText}\n`);
/** Ask for the title as soon as a prompt is sent to the CLI, instead of
* waiting for `idle`: `idle` comes only after the whole turn and its
* background tasks, often minutes after the CLI stored its title. The CLI
* answers `generate_session_title` while the turn runs, and with the title
* it stored or is generating, so this makes no second model call. The
* prompt alone is the description, because the CLI titles from the prompt
* alone. When this yields no title, the stored summary is published and
* the next prompt tries again. */
onPromptQueued(session: Session, prompt: PromptRequest["prompt"]): void {
this.askedAtPrompt = false;
// `session.cancelled` is not checked: after a cancelled turn it stays set
// until the next turn starts, which is after this prompt is queued.
if (this.settled || this.explicit.active || session.queryClosed) {
return;
}
if (!supportsTitleGeneration(session.query)) {
return;
}
const description = promptText(prompt).trim();
if (description.length < MIN_TITLE_CONTEXT_LENGTH) {
return;
}
this.askedAtPrompt = true;
this.startGeneration(() => this.titleFromPrompt(session, description));
}
/** Collect the assistant's own answer for the title. Chunks are appended
@@ -152,6 +189,7 @@ export class SessionTitles {
* `conversation_reset`, which mounts a fresh transcript. */
reset(): void {
this.settled = false;
this.askedAtPrompt = false;
this.explicit.reset();
this.context = undefined;
this.lastTitle = undefined;
@@ -165,10 +203,12 @@ export class SessionTitles {
* title — a user `/rename` or one generated earlier; the SDK folds both into
* that one field. Adopt it and latch, so neither is ever titled over.
*
* With no title yet, ask the SDK to generate one in the background: it is a
* ~2s small-model call and turn-end must not wait on it. Only when that is
* not possible do we fall back to `summary`, which for an SDK-driven session
* is just the raw first prompt. */
* With no title yet, and none asked for at the latest prompt (see
* {@link onPromptQueued}), ask the SDK to generate one in the background
* from the prompts and replies so far: it is a ~2s small-model call and
* turn-end must not wait on it. That covers a prompt too short to title
* from, such as "hi". Only when that is not possible do we fall back to
* `summary`, which for an SDK-driven session is just the raw first prompt. */
async onTurnEnd(session: Session): Promise<void> {
if (this.explicit.settledByClient) {
// No title is generated over the one the client set, but a custom title
@@ -200,17 +240,9 @@ export class SessionTitles {
? { title: info.summary, lastModified: info.lastModified }
: undefined;
if (this.canRequest(session)) {
this.settled = true;
this.explicit.track(
this.requestGenerateTitle(session, fallback).catch((error) => {
this.agent.logger.error(
`Session ${this.sessionId}: session title update failed: ${error}`,
);
}),
);
if (!this.askedAtPrompt && this.canRequest(session)) {
const description = this.context?.trim() ?? "";
this.startGeneration(() => this.requestGenerateTitle(session, description, fallback));
return;
}
// Only while this session has no title of its own: `summary` degrades to the
@@ -309,17 +341,48 @@ export class SessionTitles {
return (this.context?.trim().length ?? 0) >= MIN_TITLE_CONTEXT_LENGTH;
}
/** Generate a title from the session's own text and publish it. `persist`
* writes it to the session file, which is what carries it into
* `session/list` and `session/load`. A null or failed generation releases the
* latch so a later turn retries, and publishes `fallback` (the stored
* summary) so a backend that can't generate titles is no worse off than
* before. */
/** Latch the title and run `generation` in the background, so neither the
* prompt nor turn-end waits on a model call. A client title change waits
* for it to finish (see {@link ExplicitTitle.track}). */
private startGeneration(generation: () => Promise<void>): void {
this.settled = true;
this.explicit.track(
generation().catch((error) => {
this.agent.logger.error(`Session ${this.sessionId}: session title update failed: ${error}`);
}),
);
}
/** The prompt-time title: the one the session file already holds (a resumed
* session's title, or a `/rename`), else one generated from `description`.
* The file is read first because the CLI checks only the titles it holds in
* memory before generating one. */
private async titleFromPrompt(session: Session, description: string): Promise<void> {
const info = await this.readSessionInfo(session);
if (this.agent.sessions[this.sessionId] !== session || this.explicit.active) {
return;
}
if (info?.customTitle) {
this.context = undefined;
await this.publish(info.customTitle, info.lastModified);
return;
}
const fallback = info?.summary
? { title: info.summary, lastModified: info.lastModified }
: undefined;
await this.requestGenerateTitle(session, description, fallback);
}
/** Generate a title from `description` and publish it. `persist` writes it
* to the session file, which is what carries it into `session/list` and
* `session/load`. A null or failed generation releases the latch so a later
* prompt or turn end retries, and publishes `fallback` (the stored summary)
* so a backend that can't generate titles is no worse off than before. */
private async requestGenerateTitle(
session: Session,
description: string,
fallback: TitleFallback | undefined,
): Promise<void> {
const description = this.context?.trim() ?? "";
let title: string | null = null;
try {

View File

@@ -50,6 +50,7 @@ export class SettingsManager {
private cwd: string;
private effective: Settings = {};
private managedDeniedModels: string[] = [];
private managedApiKeyHelper?: string;
private watchers: fs.FSWatcher[] = [];
private onChange?: () => void;
private logger: { log: (...args: any[]) => void; error: (...args: any[]) => void };
@@ -111,10 +112,15 @@ export class SettingsManager {
this.managedDeniedModels = resolved.sources
.filter((entry) => entry.source === "managed")
.flatMap((entry) => entry.settings.deniedModels ?? []);
this.managedApiKeyHelper =
resolved.provenance.apiKeyHelper?.source === "managed"
? resolved.effective.apiKeyHelper
: undefined;
} catch (error) {
this.logger.error("Failed to resolve settings:", error);
this.effective = {};
this.managedDeniedModels = [];
this.managedApiKeyHelper = undefined;
}
}
@@ -177,6 +183,16 @@ export class SettingsManager {
return this.effective;
}
/**
* The `apiKeyHelper` the CLI runs: managed settings outrank the `--settings`
* override, which outranks the user/project/local files.
*/
getApiKeyHelper(settingsOverride?: Settings): string | undefined {
return (
this.managedApiKeyHelper ?? settingsOverride?.apiKeyHelper ?? this.effective.apiKeyHelper
);
}
/**
* `deniedModels` entries from the managed tier, the only tier the CLI
* honors them from.

View File

@@ -1,12 +1,35 @@
import { getSessionInfo } from "@anthropic-ai/claude-agent-sdk";
import {
getSessionInfo,
getSubagentMessages,
type SessionMessage,
} from "@anthropic-ai/claude-agent-sdk";
import { createReadStream } from "node:fs";
import { readdir, readFile } from "node:fs/promises";
import path from "node:path";
import readline from "node:readline";
import { isNativeSubagentControlTool, type HistoricalSubagent } from "./native-subagents.js";
import { findTranscript } from "./resumed-session.js";
/** What the metadata file of a subagent says about it. */
export interface SubagentRecord {
/** The SDK agent id, the name of its transcript. */
agentId: string;
/** The `name` that the Agent call gave it. */
name?: string;
/** Its role, the `subagent_type` of the Agent call. */
agentType?: string;
/** The short `description` of the Agent call. */
description?: string;
/** Whether the Agent call ran it in the background. */
background: boolean;
/** Whether the user stopped it. */
stoppedByUser: boolean;
}
/** Where the subagent transcripts of a session are. */
export interface SubagentHistory {
/** The subagent id of each Agent or Task tool use that launched a subagent. */
ids: Map<string, string>;
/** The subagent of each Agent or Task tool use that launched one. */
agents: Map<string, SubagentRecord>;
/**
* The project directory of the session, as the SDK takes it in `dir`. With
* it, the SDK reads a subagent transcript without a search of every project.
@@ -23,32 +46,278 @@ export interface SubagentHistory {
* The ids are empty for a session without a local transcript.
*/
export async function subagentHistory(sessionId: string): Promise<SubagentHistory> {
const ids = new Map<string, string>();
const agents = new Map<string, SubagentRecord>();
const transcript = await findTranscript(sessionId);
if (!transcript) return { ids };
const directory = path.join(path.dirname(transcript), sessionId, "subagents");
if (!transcript) return { agents };
const directory = subagentsDirectory(transcript, sessionId);
let names: string[];
try {
names = await readdir(directory);
} catch {
return { ids };
return { agents };
}
await Promise.all(
names.map(async (name) => {
const match = /^agent-(.+)\.meta\.json$/.exec(name);
if (!match) return;
try {
const meta: unknown = JSON.parse(await readFile(path.join(directory, name), "utf8"));
const toolUseId = (meta as { toolUseId?: unknown } | null)?.toolUseId;
if (typeof toolUseId === "string") ids.set(toolUseId, match[1]!);
const meta = JSON.parse(await readFile(path.join(directory, name), "utf8")) as Record<
string,
unknown
> | null;
if (typeof meta?.toolUseId !== "string") return;
const text = (value: unknown) => (typeof value === "string" ? value : undefined);
agents.set(meta.toolUseId, {
agentId: match[1]!,
name: text(meta.name),
agentType: text(meta.agentType),
description: text(meta.description),
background: meta.requestShape === "background",
stoppedByUser: meta.stoppedByUser === true,
});
} catch {
// A damaged meta file leaves its subagent without history.
}
}),
);
if (ids.size === 0) return { ids };
if (agents.size === 0) return { agents };
// The folder name encodes the project directory, and the encoding loses
// characters. The session info reads it back from the transcript.
const dir = (await getSessionInfo(sessionId).catch(() => undefined))?.cwd;
return dir ? { ids, dir } : { ids };
return dir ? { agents, dir } : { agents };
}
/** The folder of the subagent transcripts, next to the session transcript `transcript`. */
function subagentsDirectory(transcript: string, sessionId: string): string {
return path.join(path.dirname(transcript), sessionId, "subagents");
}
/**
* The agent that the Agent or Task call `launchId` launched, from the subagent
* history of the session `sessionId`.
*/
export async function subagentLaunchedBy(
sessionId: string,
launchId: string,
): Promise<string | undefined> {
return (await subagentHistory(sessionId)).agents.get(launchId)?.agentId;
}
/**
* The agent `agentId` and its ancestors, root first, from the subagent history
* of the session `sessionId`. The runtime restores an agent from it when Claude
* Code resumes one that ran before the session was loaded or resumed.
*
* The metadata file of an agent names the call that launched it, but not the
* agent that made the call. So the parent of a launch is the session whose
* transcript has the call, as in the replay: the session's own transcript, or
* an agent's. The list is empty when the agent or one of its ancestors has no
* metadata, or no transcript has its launch.
*/
export async function subagentLineage(
sessionId: string,
agentId: string,
logger: { error(...args: unknown[]): void },
): Promise<HistoricalSubagent[]> {
const history = await subagentHistory(sessionId);
const launchOf = new Map<string, { launchId: string; record: SubagentRecord }>();
for (const [launchId, record] of history.agents) {
launchOf.set(record.agentId, { launchId, record });
}
if (!launchOf.has(agentId)) return [];
// The SDK's readers return only the active chain, which starts after the
// last compaction, so the launch calls come from the transcript files.
const transcript = await findTranscript(sessionId);
if (!transcript) return [];
const rootLaunches = await readLaunchIdsFromTranscript(transcript, null, logger);
const agentLaunches = new Map<string, Set<string>>();
/** The agent whose transcript has the launch `launchId`, `null` for the root's. */
const launcherOf = async (launchId: string): Promise<string | null | undefined> => {
if (rootLaunches.has(launchId)) return null;
for (const candidate of launchOf.keys()) {
let launches = agentLaunches.get(candidate);
if (!launches) {
launches = await readLaunchIdsFromTranscript(
path.join(subagentsDirectory(transcript, sessionId), `agent-${candidate}.jsonl`),
candidate,
logger,
);
agentLaunches.set(candidate, launches);
}
if (launches.has(launchId)) return candidate;
}
return undefined;
};
const lineage: HistoricalSubagent[] = [];
let current: string | null = agentId;
while (current !== null) {
const launch = launchOf.get(current);
// A cycle, or an ancestor without metadata.
if (!launch || lineage.some((entry) => entry.agentId === current)) return [];
const launcher = await launcherOf(launch.launchId);
if (launcher === undefined) return [];
lineage.unshift({
agentId: current,
launchId: launch.launchId,
...(launcher !== null ? { parentAgentId: launcher } : {}),
...(launch.record.name ? { name: launch.record.name } : {}),
...(launch.record.agentType ? { agentType: launch.record.agentType } : {}),
...(launch.record.description ? { description: launch.record.description } : {}),
});
current = launcher;
}
return lineage;
}
/** The ids of the Agent and Task calls that launched subagents in `content`. */
export function subagentLaunchIds(content: unknown): string[] {
if (!Array.isArray(content)) return [];
return content.flatMap((block) =>
typeof block === "object" &&
block !== null &&
block.type === "tool_use" &&
typeof block.id === "string" &&
isNativeSubagentControlTool(block.name)
? [block.id as string]
: [],
);
}
/**
* The ids of the launch calls in the transcript `file`, every record of it,
* including those before a compaction. `owner` is the agent the transcript
* belongs to, null for the session's. The session's transcript can hold
* sidechain records, which are an agent's, not the root's; an agent's
* transcript keeps only the records of that agent. A file that cannot be read
* has no launches, so the agent is not placed under the root.
*/
async function readLaunchIdsFromTranscript(
file: string,
owner: string | null,
logger: { error(...args: unknown[]): void },
): Promise<Set<string>> {
const launches = new Set<string>();
const stream = createReadStream(file, { encoding: "utf8" });
try {
for await (const line of readline.createInterface({ input: stream, crlfDelay: Infinity })) {
const forkDirective = owner !== null && line.includes(FORK_BOILERPLATE_OPEN);
if (!line.includes('"tool_use"') && !forkDirective) continue;
let record: {
type?: unknown;
isSidechain?: unknown;
agentId?: unknown;
message?: { content?: unknown };
} | null;
try {
record = JSON.parse(line);
} catch {
continue;
}
// Every record of a fork child's transcript before its directive message
// is inherited context: a copy of the launching message, whose launch
// calls are the parent's, not the fork's.
if (forkDirective && record !== null && isForkDirectiveMessage(record)) {
launches.clear();
continue;
}
if (record?.type !== "assistant") continue;
if (
owner === null
? record.isSidechain === true
: record.agentId !== undefined && record.agentId !== owner
) {
continue;
}
for (const id of subagentLaunchIds(record.message?.content)) launches.add(id);
}
} catch (error) {
logger.error(`Failed to read the transcript ${file}:`, error);
return new Set();
} finally {
stream.destroy();
}
return launches;
}
/**
* The transcript of the subagent `agentId` of a session, or an empty list
* when it cannot be read. With the project directory, the SDK skips its
* search of every project; a session opened from another directory falls
* back to that search.
*/
export async function readSubagentMessages(
sessionId: string,
agentId: string,
history: SubagentHistory,
logger: { error(...args: unknown[]): void },
): Promise<SessionMessage[]> {
try {
if (history.dir) {
const messages = await getSubagentMessages(sessionId, agentId, { dir: history.dir });
if (messages.length > 0) return stripForkPrefix(messages);
}
return stripForkPrefix(await getSubagentMessages(sessionId, agentId));
} catch (error) {
logger.error(`Failed to read the history of subagent ${agentId}:`, error);
return [];
}
}
/** How Claude Code opens the directive message of a `subagent_type: "fork"` child. */
const FORK_BOILERPLATE_OPEN = "<fork-boilerplate>";
/** What follows the boilerplate, right before the prompt of the Agent call. */
const FORK_DIRECTIVE_MARKER = "</fork-boilerplate>\nYour directive: ";
/** Whether `block` is the text block of a fork child's directive message. */
function isForkDirectiveText(block: unknown): block is { type: "text"; text: string } {
return (
typeof block === "object" &&
block !== null &&
(block as { type?: unknown }).type === "text" &&
typeof (block as { text?: unknown }).text === "string" &&
(block as { text: string }).text.startsWith(FORK_BOILERPLATE_OPEN)
);
}
/** Whether the transcript record `message` is the directive message of a fork child. */
function isForkDirectiveMessage(message: { type?: unknown; message?: unknown }): boolean {
if (message.type !== "user") return false;
const content = (message.message as { content?: unknown } | null | undefined)?.content;
return Array.isArray(content) && content.some(isForkDirectiveText);
}
/**
* The transcript of a `subagent_type: "fork"` child without the context it
* inherited. A fork transcript starts with a copy of the parent's launching
* assistant message (its text, thinking and every tool_use, including the
* Agent call itself), then a user message with a "Fork started — processing
* in background" tool_result for each of those tool_uses and the text
* `<fork-boilerplate>…</fork-boilerplate>\nYour directive: <prompt>`. A fork
* launched from inside a subagent also gets a copy of that subagent's history
* before them. None of it is the child's own work: replayed as is, the
* parent's text and tool calls would show again in the child.
*
* This drops every record before the directive message, and reduces that
* message to the prompt. Other transcripts are returned unchanged.
*/
export function stripForkPrefix(messages: SessionMessage[]): SessionMessage[] {
const index = messages.findIndex(isForkDirectiveMessage);
if (index === -1) return messages;
const directive = messages[index];
const content = (directive.message as { content: unknown[] }).content;
const prompt = content.flatMap((block) => {
if (!isForkDirectiveText(block)) return [];
const start = block.text.indexOf(FORK_DIRECTIVE_MARKER);
return start === -1
? []
: [{ type: "text", text: block.text.slice(start + FORK_DIRECTIVE_MARKER.length) }];
});
const rest = messages.slice(index + 1);
if (prompt.length === 0) return rest;
return [
{ ...directive, message: { ...(directive.message as object), content: prompt } },
...rest,
];
}

421
src/subagent-replay.ts Normal file
View File

@@ -0,0 +1,421 @@
/**
* The replay of subagents to a client on the subagents RFD form.
*
* Claude Code keeps each subagent's history in its own transcript, and the
* subagent's metadata file names the Agent or Task call that launched it (see
* `subagentHistory`). The replay announces each child right after that call,
* under the session id it had live, the SDK agent id, and sends the child's
* history to the child session. Every child's end goes out before the replay
* ends, since no subagent from before the reload still runs.
*
* As live, the parent is not shown the Agent or Task call of a child that the
* replay announces: the launch's prompt is the parent's outgoing message, and
* a foreground child's answer is its reply to the parent. A child's
* successful SubagentHandback call is its message to the parent instead.
* Unlike live, the replay does not tell runs apart, so a child that handed
* back any report gets no reply from its launch's result.
*
* A launch without metadata has no known id, so it is not announced: the RFD
* forbids inventing one. Its Agent call stays an ordinary tool call of the
* parent, and a notice says that subagent history is missing.
*/
import type { SessionMessage } from "@anthropic-ai/claude-agent-sdk";
import type { AcpSessionNotification, SubagentState } from "./acp-subagents.js";
import {
HANDBACK_TOOL,
handbackMessage,
isNativeSubagentControlTool,
launchReply,
linkedSessionMessages,
nativeSubagentState,
rfdEndState,
rfdLabels,
type WorkState,
} from "./native-subagents.js";
import { noticeUpdate } from "./session-notices.js";
import type { SubagentHistory, SubagentRecord } from "./subagent-history.js";
type Publish = (notification: AcpSessionNotification) => Promise<void>;
/** A child that the replay shows: its launch has metadata. */
export type ReplayChild = {
/** The Agent or Task tool use that launched it. */
launchId: string;
/** Its session id: the SDK agent id, as live. */
sessionId: string;
record: SubagentRecord;
/** The session that made the launch. */
parentSessionId: string;
/** The input of the launch call. */
input: Record<string, unknown>;
/** How the launch's tool result ended a foreground child's run. */
launchEnd?: SubagentState;
/** The state of the child's last persisted task notification. */
notified?: SubagentState;
announced: boolean;
promptSent: boolean;
/** Whether it handed a report to its parent with a SubagentHandback call. */
handedBack?: boolean;
};
type SendMessageCall = {
toolUseId: string;
to: string;
message: string;
senderSessionId: string;
delivered: boolean;
};
export class RfdSubagentReplay {
/** The children by the id of the tool use that launched each. */
private readonly children = new Map<string, ReplayChild>();
private readonly byAgentId = new Map<string, ReplayChild>();
private readonly sendMessages: SendMessageCall[] = [];
/** Launches without metadata, which the replay leaves out. */
private readonly omitted = new Set<string>();
/** Children whose transcript could not be read: only their association is replayed. */
private withoutHistory = 0;
/** The SubagentHandback calls, with their report and whether their result failed. */
private readonly handbacks = new Map<
string,
{ message: string; failed: boolean; sent: boolean }
>();
constructor(
private readonly history: SubagentHistory,
private readonly publish: Publish,
) {}
/**
* Registers the launches, the launch results, and the SendMessage calls in
* `messages`, the transcript of the session `ownerSessionId`.
*/
scan(messages: readonly SessionMessage[], ownerSessionId: string): void {
for (const message of messages) {
for (const block of contentBlocks(message)) {
if (block.type === "tool_use" && typeof block.id === "string") {
if (isNativeSubagentControlTool(block.name)) {
this.registerLaunch(block.id, inputOf(block), ownerSessionId);
} else if (block.name === HANDBACK_TOOL) {
const message = handbackMessage(block.input);
if (message) this.handbacks.set(block.id, { message, failed: false, sent: false });
} else if (block.name === "SendMessage") {
const input = inputOf(block);
if (typeof input.to === "string" && typeof input.message === "string") {
this.sendMessages.push({
toolUseId: block.id,
to: input.to,
message: input.message,
senderSessionId: ownerSessionId,
delivered: false,
});
}
}
} else if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
const handback = this.handbacks.get(block.tool_use_id);
if (handback && block.is_error === true) handback.failed = true;
const child = this.children.get(block.tool_use_id);
// A background launch answers at once; its result is not the end.
if (child && !child.record.background) {
child.launchEnd = replaySubagentTerminalState(block);
}
}
}
}
}
/** The child that the tool use `toolUseId` launched, when the replay shows it. */
childOfLaunch(toolUseId: string): ReplayChild | undefined {
return this.children.get(toolUseId);
}
/** Announces `child` on its parent, as live: running, with the parent's labels. */
async announce(child: ReplayChild): Promise<void> {
if (child.announced) return;
const labels = rfdLabels(
child.record.name ?? child.input.name,
child.record.agentType ?? child.input.subagent_type,
child.record.description ?? child.input.description,
);
await this.publish({
sessionId: child.parentSessionId,
update: {
sessionUpdate: "subagent_update",
sessionId: child.sessionId,
...(labels.title ? { title: labels.title } : {}),
...(labels.description ? { description: labels.description } : {}),
state: { state: "running" },
},
});
child.announced = true;
}
/**
* The rest of a child's user message to replay, or `null` when nothing is
* left, after its text went out as the child's incoming `session_message`.
*
* A child session has no user, so the text of its user messages is never a
* user message. The first text is the prompt of the launch. A message that
* a SendMessage call delivered (`origin.kind: "coordinator"`) is another
* incoming message. Any other text is harness text, such as "[Request
* interrupted by user for tool use]", which the live child session does not
* show. Tool results stay.
*/
async childContent(
child: ReplayChild,
message: SessionMessage,
content: unknown,
): Promise<unknown> {
const text = textOf(content);
if (text.trim()) {
if (isDelivery(message)) await this.sendDelivery(child, message, text);
else if (!child.promptSent) await this.sendPrompt(child, text);
}
if (!Array.isArray(content)) return null;
const rest = content.filter(
(block) => !(typeof block === "object" && block !== null && block.type === "text"),
);
return rest.length > 0 ? rest : null;
}
/**
* Ends the replay of `child`'s history. `messages` is its transcript: when
* it is empty, the replay has only the child's association, and the prompt
* comes from the launch call.
*/
async childReplayed(child: ReplayChild, messages: readonly SessionMessage[]): Promise<void> {
if (messages.length === 0) this.withoutHistory++;
if (!child.promptSent && typeof child.input.prompt === "string") {
await this.sendPrompt(child, child.input.prompt);
}
}
/**
* Takes a replayed update of a tool call, from the transcript of `owner`
* when it is a child's. Returns whether the client is not shown it: an
* update of the launch of a child that the replay shows, or of a successful
* SubagentHandback call of `owner`. A foreground launch's completed result
* goes out as the child's reply, and a hand-back as the child's message to
* its parent.
*/
async toolCallUpdate(
update: AcpSessionNotification["update"],
owner: ReplayChild | undefined,
): Promise<boolean> {
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update") {
return false;
}
const handback = owner ? this.handbacks.get(update.toolCallId) : undefined;
if (owner && handback && !handback.failed) {
if (!handback.sent) {
handback.sent = true;
owner.handedBack = true;
for (const view of linkedSessionMessages(
update.toolCallId,
owner.sessionId,
owner.parentSessionId,
handback.message,
)) {
await this.publish(view);
}
}
return true;
}
const child = this.children.get(update.toolCallId);
if (!child) return false;
if (
update.status === "completed" &&
child.announced &&
!child.record.background &&
!child.handedBack
) {
const reply = launchReply(child.launchId, child.sessionId, child.parentSessionId, update);
if (reply) await this.publish(reply);
}
return true;
}
/**
* Takes a persisted task notification. Returns whether it was a child's:
* its task id is the agent id.
*/
taskNotification(taskId: string, status: string): boolean {
const child = this.byAgentId.get(taskId);
if (!child) return false;
child.notified = nativeSubagentState(status) ?? child.notified;
return true;
}
/**
* Sends the end of every announced child, and a notice when subagent history
* is missing. The end is the state of the child's last task notification,
* else `cancelled` for a child the user stopped, else the result of its
* foreground launch, else `idle` with no stop reason.
*/
async finish(supportsNotices: boolean, rootSessionId: string): Promise<void> {
for (const child of [...this.children.values()].reverse()) {
if (!child.announced) continue;
const end = child.notified ?? (child.record.stoppedByUser ? "cancelled" : child.launchEnd);
const state: WorkState = end ? rfdEndState(end, undefined) : { state: "idle" };
await this.publish({
sessionId: child.parentSessionId,
update: { sessionUpdate: "subagent_update", sessionId: child.sessionId, state },
});
}
const missing = this.omitted.size + this.withoutHistory;
if (missing > 0 && supportsNotices) {
await this.publish({
sessionId: rootSessionId,
update: noticeUpdate({
severity: "info",
title: "Some subagent history is not available",
description:
missing === 1
? "The history of 1 subagent of this session could not be restored."
: `The history of ${missing} subagents of this session could not be restored.`,
}),
});
}
}
private registerLaunch(
toolUseId: string,
input: Record<string, unknown>,
parentSessionId: string,
): void {
if (this.children.has(toolUseId)) return;
const record = this.history.agents.get(toolUseId);
if (!record) {
this.omitted.add(toolUseId);
return;
}
const child: ReplayChild = {
launchId: toolUseId,
sessionId: record.agentId,
record,
parentSessionId,
input,
announced: false,
promptSent: false,
};
this.children.set(toolUseId, child);
this.byAgentId.set(record.agentId, child);
}
/** The prompt of the launch, with the launch's id, to the parent and the child, as live. */
private async sendPrompt(child: ReplayChild, text: string): Promise<void> {
child.promptSent = true;
for (const view of linkedSessionMessages(
child.launchId,
child.parentSessionId,
child.sessionId,
text,
)) {
await this.publish(view);
}
}
/**
* A message that a SendMessage call delivered. Claude Code frames it for the
* child's model ("The coordinator sent a message while you were working:",
* then the message, then an instruction), so the call it came from is the
* first undelivered SendMessage to the child whose message the delivered
* text contains. With that call, the message has the call's id and text and
* its sender, as live; without it, its transcript id and the delivered text.
*/
private async sendDelivery(
child: ReplayChild,
message: SessionMessage,
text: string,
): Promise<void> {
const names = [child.sessionId, child.record.name, child.input.name];
const call = this.sendMessages.find(
(candidate) =>
!candidate.delivered &&
names.includes(candidate.to) &&
text.includes(candidate.message.trim()),
);
if (call) {
call.delivered = true;
await this.sendMessage(child, call.toolUseId, call.message, call.senderSessionId);
} else {
await this.sendMessage(child, message.uuid, text, undefined);
}
}
private async sendMessage(
child: ReplayChild,
messageId: string,
text: string,
sender: string | undefined,
): Promise<void> {
await this.publish({
sessionId: child.sessionId,
update: {
sessionUpdate: "session_message",
messageId,
...(sender ? { senderSessionId: sender } : {}),
recipientSessionId: child.sessionId,
content: [{ type: "text", text }],
},
});
}
}
/**
* The end of a foreground launch from its tool result: an error result that
* names a stop is a cancellation, any other error a failure.
*/
export function replaySubagentTerminalState(
block: Record<string, unknown>,
): "completed" | "failed" | "cancelled" {
if (block.is_error !== true) return "completed";
const text = replayContentText(block.content).toLowerCase();
return /\b(?:cancelled|canceled|interrupted|stopped|killed)\b/.test(text)
? "cancelled"
: "failed";
}
function replayContentText(value: unknown, seen = new Set<unknown>()): string {
if (typeof value === "string") return value;
if (typeof value !== "object" || value === null || seen.has(value)) return "";
seen.add(value);
if (Array.isArray(value)) return value.map((item) => replayContentText(item, seen)).join(" ");
const record = value as Record<string, unknown>;
return [record.text, record.content, record.message]
.map((item) => replayContentText(item, seen))
.filter(Boolean)
.join(" ");
}
function contentBlocks(message: SessionMessage): Record<string, unknown>[] {
const content = (message as { message?: { content?: unknown } }).message?.content;
return Array.isArray(content)
? content.filter(
(block): block is Record<string, unknown> => typeof block === "object" && block !== null,
)
: [];
}
function inputOf(block: Record<string, unknown>): Record<string, unknown> {
return typeof block.input === "object" && block.input !== null
? (block.input as Record<string, unknown>)
: {};
}
/** The text of user message content: a string, or its text blocks. */
function textOf(content: unknown): string {
if (typeof content === "string") return content;
if (!Array.isArray(content)) return "";
return content
.flatMap((block) =>
typeof block === "object" && block !== null && block.type === "text" ? [block.text] : [],
)
.filter((text): text is string => typeof text === "string")
.join("\n");
}
/** A message that a SendMessage call delivered to the child. */
function isDelivery(message: SessionMessage): boolean {
return (message as { origin?: { kind?: unknown } | null }).origin?.kind === "coordinator";
}

75
src/task-notifications.ts Normal file
View File

@@ -0,0 +1,75 @@
/**
* The `<task-notification>` inputs that Claude Code writes to a transcript
* when a background task stops. The replay of a session (`acp-agent.ts`) and
* the session index (`session-index/background-tasks.ts`) read them here, so
* both read the same fields.
*/
export const TASK_NOTIFICATION_TAG = "task-notification";
/** The text between the first `<tag>` and the next `</tag>` after it. */
export function markerTagText(text: string, tag: string): string | undefined {
const open = `<${tag}>`;
const close = `</${tag}>`;
const start = text.indexOf(open);
if (start === -1) return undefined;
const end = text.indexOf(close, start + open.length);
if (end === -1) return undefined;
return text.slice(start + open.length, end);
}
/** The fields of a persisted `<task-notification>`, in the shape of the SDK
* `task_notification` frame that reported the same stop live. */
export type PersistedTaskNotification = {
task_id: string;
status: string;
tool_use_id?: string;
summary?: string;
output_file?: string;
};
/**
* Returns each `<task-notification>` in user-message content (a string or its
* text blocks). The `<result>` body is model context that the live frame does
* not carry, so the scan stops at it and a tag quoted in it cannot match.
*
* Only a notification with a `<task-id>` and a `<status>` is returned. A
* notification without a status comes while its task still runs: the
* check-in of a quiet shell ("is still running after …") or a `Monitor`
* event.
*/
export function taskNotificationsOf(content: unknown): PersistedTaskNotification[] {
const texts =
typeof content === "string"
? [content]
: Array.isArray(content)
? content.flatMap((block) =>
block?.type === "text" && typeof block.text === "string" ? [block.text as string] : [],
)
: [];
const open = `<${TASK_NOTIFICATION_TAG}>`;
const close = `</${TASK_NOTIFICATION_TAG}>`;
const notifications: PersistedTaskNotification[] = [];
for (const text of texts) {
let start = text.indexOf(open);
while (start !== -1) {
const end = text.indexOf(close, start + open.length);
if (end === -1) break;
const body = text.slice(start + open.length, end).split("<result>", 1)[0];
const field = (tag: string) => markerTagText(body, tag)?.trim() || undefined;
const taskId = field("task-id");
const status = field("status");
if (taskId && status) {
notifications.push({
task_id: taskId,
status,
tool_use_id: field("tool-use-id"),
summary: field("summary"),
output_file: field("output-file"),
});
}
start = text.indexOf(open, end + close.length);
}
}
return notifications;
}

View File

@@ -515,7 +515,7 @@ describe("ClaudeAcpAgent settings", () => {
_meta: { disableBuiltInTools: true },
});
await agent.setSessionMode({ sessionId, modeId: "default" });
const { RequestError } = await import("@agentclientprotocol/sdk");
const { RequestError } = await import("@agentclientprotocol/sdk/unstable");
const createSession = (agent as any).createSession.bind(agent);
const creations: object[] = [];
(agent as any).createSession = async (params: object, options: { resume?: string }) => {

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{"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"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolName":"mcp__docs__search"},"is_mcp_tool_call":true},"content":[],"kind":"other","name":"mcp__docs__search","sessionUpdate":"tool_call","status":"pending","title":"mcp__docs__search","toolCallId":"toolu_mcp"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolName":"mcp__docs__search"},"is_mcp_tool_call":true,"jetbrains":{"air":{"mcp":{"server":"docs","tool":"search"},"version":1}}},"content":[],"kind":"other","name":"mcp__docs__search","sessionUpdate":"tool_call","status":"pending","title":"mcp__docs__search","toolCallId":"toolu_mcp"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"rawInput":{"q":"acp"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_mcp"}}}
{"kind":"requestPermission","payload":{"_meta":{"jetbrains":{"air":{"permission":{"title":"mcp__docs__search","version":1},"version":1}}},"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","toolCall":{"_meta":{"claudeCode":{"mcpServer":{"name":"docs","source":"project"}}},"rawInput":{"q":"acp"},"title":"mcp__docs__search","toolCallId":"toolu_mcp"}}}

View File

@@ -1,4 +1,4 @@
{"kind":"initialize","payload":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","customInstructions","recommendedValue","diffPatch","planFile"],"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":{}}},"agentInfo":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"authMethods":[],"protocolVersion":1}}
{"kind":"initialize","payload":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","customInstructions","recommendedValue","diffPatch","planFile","backgroundSubagents"],"goal":{"actions":["set","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"logout":{}},"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":"sessionUpdate","payload":{"sessionId":"<session>","update":{"content":{"text":"Fix the bug","type":"text"},"messageId":"00000000-0000-4000-8000-00000000c001","sessionUpdate":"user_message_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"content":{"text":"Check the file.","type":"text"},"messageId":"msg_r1","sessionUpdate":"agent_thought_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"content":{"text":"I will edit it.","type":"text"},"messageId":"msg_r1","sessionUpdate":"agent_message_chunk"}}}

View File

@@ -1,3 +1,3 @@
{"kind":"initialize","payload":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","customInstructions","recommendedValue","diffPatch","planFile"],"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":{}}},"agentInfo":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"authMethods":[],"protocolVersion":1}}
{"kind":"initialize","payload":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","customInstructions","recommendedValue","diffPatch","planFile","backgroundSubagents"],"goal":{"actions":["set","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"auth":{"logout":{}},"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":"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,7 +1,6 @@
{"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"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolName":"Agent","toolResponse":{"isAsync":true,"status":"async_launched"}}},"sessionUpdate":"tool_call_update","toolCallId":"toolu_agent"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"capabilities":{},"name":"Background review","prompt":"Review the diff","sessionUpdate":"subagent_spawned","subagentSessionId":"a2","task":"Review the diff"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"a2","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_agent"}},"content":{"text":"Reviewi","type":"text"},"messageId":"msg_async_sub","sessionUpdate":"agent_message_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"a2","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_agent"}},"content":{"text":"ng now.","type":"text"},"messageId":"msg_async_sub","sessionUpdate":"agent_message_chunk"}}}

View File

@@ -18,10 +18,9 @@
{"kind":"sessionUpdate","payload":{"sessionId":"agent_1","update":{"content":[{"newText":"b","oldText":"a","path":"<cwd>/x.ts","type":"diff"}],"rawInput":{"file_path":"<cwd>/x.ts"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_sub_edit"}}}
{"kind":"requestPermission","payload":{"_meta":{"jetbrains":{"air":{"permission":{"title":"Edit x.ts","version":1},"version":1}}},"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"agent_1","toolCall":{"rawInput":{"file_path":"<cwd>/x.ts"},"title":"Edit x.ts","toolCallId":"toolu_sub_edit"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_1","update":{"rawOutput":"The file has been updated.","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_sub_edit"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"locations":[{"line":1,"path":"<cwd>/x.ts"}],"sessionUpdate":"tool_call_update","toolCallId":"toolu_sub_edit"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_1","update":{"locations":[{"line":1,"path":"<cwd>/x.ts"}],"sessionUpdate":"tool_call_update","toolCallId":"toolu_sub_edit"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":{"text":"Foun","type":"text"},"messageId":"msg_sub_2","sessionUpdate":"agent_message_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":{"text":"d it.","type":"text"},"messageId":"msg_sub_2","sessionUpdate":"agent_message_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolName":"Task","toolResponse":{"status":"completed"}}},"sessionUpdate":"tool_call_update","toolCallId":"toolu_task"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"sessionUpdate":"subagent_state_update","state":"completed","subagentSessionId":"agent_1"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"promptResponse","payload":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"/goal Ship the feature","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"messageId":"00000000-0000-4000-8000-00000000c001","sessionUpdate":"user_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":{"text":"Fix the bug","type":"text"},"messageId":"00000000-0000-4000-8000-00000000c001","sessionUpdate":"user_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"messageId":"msg_r1","sessionUpdate":"agent_thought"}}}
@@ -16,4 +16,6 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Report.","type":"text"},"type":"content"}],"rawOutput":"Report.","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_r_task"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"messageId":"msg_r2","sessionUpdate":"agent_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":{"text":"Fixed.","type":"text"},"messageId":"msg_r2","sessionUpdate":"agent_message_chunk"}}}
{"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":"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"}],"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"}]}}

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":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,18 +1,17 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"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":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"kind":"think","name":"Agent","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Task","toolCallId":"toolu_agent"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Background review"},"sessionUpdate":"tool_call_update","title":"Background review","toolCallId":"toolu_agent"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Background review","prompt":"Review the diff"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_agent"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Background review","prompt":"Review the diff","subagent_type":"general-purpose"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_agent"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Review the diff","type":"text"},"type":"content"}],"rawInput":{"description":"Background review","prompt":"Review the diff","run_in_background":true,"subagent_type":"general-purpose"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_agent"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Async agent launched successfully.","type":"text"},"type":"content"}],"rawOutput":"Async agent launched successfully.\nagentId: a2 (use SendMessage to continue)","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_agent"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolResponse":{"isAsync":true,"status":"async_launched"}}},"sessionUpdate":"tool_call_update","toolCallId":"toolu_agent"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_agent"}},"content":{"text":"Reviewi","type":"text"},"messageId":"msg_async_sub","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_agent"}},"content":{"text":"ng now.","type":"text"},"messageId":"msg_async_sub","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"capabilities":{"cancel":{}},"description":"Background review","sessionId":"a2","sessionUpdate":"subagent_update","state":{"state":"running"},"title":"general-purpose"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"a2","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Review the diff","type":"text"}],"messageId":"toolu_agent","recipientMessageId":"toolu_agent","recipientSessionId":"a2","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"a2","update":{"content":[{"text":"Review the diff","type":"text"}],"messageId":"toolu_agent","recipientSessionId":"a2","senderMessageId":"toolu_agent","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"a2","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_agent"}},"content":{"text":"Reviewi","type":"text"},"messageId":"msg_async_sub","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"a2","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_agent"}},"content":{"text":"ng now.","type":"text"},"messageId":"msg_async_sub","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionId":"a2","sessionUpdate":"subagent_update","state":{"state":"idle","stopReason":"end_turn"}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"a2","update":{"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,18 +1,20 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"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":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"kind":"think","name":"Agent","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Task","toolCallId":"toolu_late"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Check the logs"},"sessionUpdate":"tool_call_update","title":"Check the logs","toolCallId":"toolu_late"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Check the logs","prompt":"Check logs"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_late"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Check logs","type":"text"},"type":"content"}],"rawInput":{"description":"Check the logs","prompt":"Check logs","subagent_type":"general-purpose"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_late"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_late"}},"content":[],"kind":"read","locations":[],"name":"Read","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Read File","toolCallId":"toolu_late_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"locations":[{"line":1,"path":"/var/log/app.log"}],"rawInput":{"file_path":"/var/log/app.log"},"sessionUpdate":"tool_call_update","title":"Read /var/log/app.log","toolCallId":"toolu_late_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"The logs are clean.","type":"text"},"type":"content"}],"rawOutput":[{"text":"The logs are clean.","type":"text"}],"sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_late"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_late","toolResponse":{"elapsedTimeSeconds":4}}},"sessionUpdate":"tool_call_update","status":"in_progress","toolCallId":"toolu_late_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"```\n1\tok\n```","type":"text"},"type":"content"}],"rawOutput":"1\tok","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_late_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"capabilities":{"cancel":{}},"description":"Check the logs","sessionId":"agent_late","sessionUpdate":"subagent_update","state":{"state":"running"},"title":"general-purpose"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Check logs","type":"text"}],"messageId":"toolu_late","recipientMessageId":"toolu_late","recipientSessionId":"agent_late","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"content":[{"text":"Check logs","type":"text"}],"messageId":"toolu_late","recipientSessionId":"agent_late","senderMessageId":"toolu_late","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_late"}},"content":[],"kind":"read","locations":[],"name":"Read","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Read File","toolCallId":"toolu_late_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"locations":[{"line":1,"path":"/var/log/app.log"}],"rawInput":{"file_path":"/var/log/app.log"},"sessionUpdate":"tool_call_update","title":"Read /var/log/app.log","toolCallId":"toolu_late_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"The logs are clean.","type":"text"}],"messageId":"toolu_late:reply","recipientSessionId":"<session>","senderSessionId":"agent_late","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionId":"agent_late","sessionUpdate":"subagent_update","state":{"state":"idle","stopReason":"end_turn"}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_late","toolResponse":{"elapsedTimeSeconds":4}}},"sessionUpdate":"tool_call_update","status":"in_progress","toolCallId":"toolu_late_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"content":[{"content":{"text":"```\n1\tok\n```","type":"text"},"type":"content"}],"rawOutput":"1\tok","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_late_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,31 +1,36 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"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":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"kind":"think","name":"Agent","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Task","toolCallId":"toolu_native"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Fix the build"},"sessionUpdate":"tool_call_update","title":"Fix the build","toolCallId":"toolu_native"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Fix the build","prompt":"Make the build pass"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_native"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Make the build pass","type":"text"},"type":"content"}],"rawInput":{"description":"Fix the build","prompt":"Make the build pass","subagent_type":"general-purpose"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_native"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"content":{"text":"Running t","type":"text"},"messageId":"msg_native_sub_1","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"content":{"text":"he build.","type":"text"},"messageId":"msg_native_sub_1","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"content":[],"kind":"search","name":"Grep","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"grep","toolCallId":"toolu_native_grep"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"pattern":"TODO"},"sessionUpdate":"tool_call_update","title":"grep \"TODO\"","toolCallId":"toolu_native_grep"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"path":"/src","pattern":"TODO"},"sessionUpdate":"tool_call_update","title":"grep \"TODO\" /src","toolCallId":"toolu_native_grep"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"src/a.ts","type":"text"},"type":"content"}],"rawOutput":"src/a.ts","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_native_grep"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"terminal_update","terminalId":"toolu_native_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"content":[{"terminalId":"toolu_native_bash","type":"terminal"}],"kind":"execute","name":"Bash","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Terminal","toolCallId":"toolu_native_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"command":"make","sessionUpdate":"terminal_update","terminalId":"toolu_native_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"command":"make"},"sessionUpdate":"tool_call_update","title":"make","toolCallId":"toolu_native_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"command":"make","description":"Build"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_native_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"capabilities":{"cancel":{}},"description":"Fix the build","sessionId":"agent_n","sessionUpdate":"subagent_update","state":{"state":"running"},"title":"general-purpose"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Make the build pass","type":"text"}],"messageId":"toolu_native","recipientMessageId":"toolu_native","recipientSessionId":"agent_n","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"content":[{"text":"Make the build pass","type":"text"}],"messageId":"toolu_native","recipientSessionId":"agent_n","senderMessageId":"toolu_native","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"content":{"text":"Running t","type":"text"},"messageId":"msg_native_sub_1","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"content":{"text":"he build.","type":"text"},"messageId":"msg_native_sub_1","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"content":[],"kind":"search","name":"Grep","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"grep","toolCallId":"toolu_native_grep"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"rawInput":{"pattern":"TODO"},"sessionUpdate":"tool_call_update","title":"grep \"TODO\"","toolCallId":"toolu_native_grep"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"rawInput":{"path":"/src","pattern":"TODO"},"sessionUpdate":"tool_call_update","title":"grep \"TODO\" /src","toolCallId":"toolu_native_grep"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"content":[{"content":{"text":"src/a.ts","type":"text"},"type":"content"}],"rawOutput":"src/a.ts","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_native_grep"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"sessionUpdate":"terminal_update","terminalId":"toolu_native_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"content":[{"terminalId":"toolu_native_bash","type":"terminal"}],"kind":"execute","name":"Bash","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Terminal","toolCallId":"toolu_native_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"command":"make","sessionUpdate":"terminal_update","terminalId":"toolu_native_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"rawInput":{"command":"make"},"sessionUpdate":"tool_call_update","title":"make","toolCallId":"toolu_native_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"rawInput":{"command":"make","description":"Build"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_native_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionId":"agent_n","sessionUpdate":"subagent_update","state":{"state":"requires_action"}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"content":[{"terminalId":"toolu_native_bash","type":"terminal"}],"kind":"execute","name":"Bash","rawInput":{"command":"make","description":"Build"},"status":"pending","title":"make","toolCallId":"toolu_native_bash"},"type":"tool_call"},"title":"make"}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"agent_n","subject":{"toolCall":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"content":[{"terminalId":"toolu_native_bash","type":"terminal"}],"kind":"execute","name":"Bash","rawInput":{"command":"make","description":"Build"},"status":"pending","title":"make","toolCallId":"toolu_native_bash"},"type":"tool_call"},"title":"make"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"output":{"data":"bWFrZTogKioqIEVycm9yIDI="},"sessionUpdate":"terminal_update","terminalId":"toolu_native_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"exitStatus":{},"sessionUpdate":"terminal_update","terminalId":"toolu_native_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"tool_call_update","status":"failed","toolCallId":"toolu_native_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"```\nThe build still fails.\n```","type":"text"},"type":"content"}],"rawOutput":[{"text":"The build still fails.","type":"text"}],"sessionUpdate":"tool_call_update","status":"failed","toolCallId":"toolu_native"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionId":"agent_n","sessionUpdate":"subagent_update","state":{"state":"running"}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"output":{"data":"bWFrZTogKioqIEVycm9yIDI="},"sessionUpdate":"terminal_update","terminalId":"toolu_native_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"exitStatus":{},"sessionUpdate":"terminal_update","terminalId":"toolu_native_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"sessionUpdate":"tool_call_update","status":"failed","toolCallId":"toolu_native_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionId":"agent_n","sessionUpdate":"subagent_update","state":{"state":"idle","stopReason":"error"}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"sessionUpdate":"state_update","state":"idle","stopReason":"error"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,24 +1,28 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"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":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"kind":"think","name":"Agent","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Task","toolCallId":"toolu_outer"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Plan the work"},"sessionUpdate":"tool_call_update","title":"Plan the work","toolCallId":"toolu_outer"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Plan the work","prompt":"Plan it"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_outer"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Plan it","type":"text"},"type":"content"}],"rawInput":{"description":"Plan the work","prompt":"Plan it","subagent_type":"Plan"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_outer"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_outer"}},"content":[],"kind":"think","name":"Agent","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Task","toolCallId":"toolu_inner"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Read the spec"},"sessionUpdate":"tool_call_update","title":"Read the spec","toolCallId":"toolu_inner"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Read the spec","prompt":"Read spec.md"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_inner"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Read spec.md","type":"text"},"type":"content"}],"rawInput":{"description":"Read the spec","prompt":"Read spec.md","subagent_type":"Explore"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_inner"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_inner"}},"content":[],"kind":"read","locations":[],"name":"Read","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Read File","toolCallId":"toolu_grandchild_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"locations":[{"line":1,"path":"/spec.md"}],"rawInput":{"file_path":"/spec.md"},"sessionUpdate":"tool_call_update","title":"Read /spec.md","toolCallId":"toolu_grandchild_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"```\n1\tspec\n```","type":"text"},"type":"content"}],"rawOutput":"1\tspec","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_grandchild_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"The spec says X.","type":"text"},"type":"content"}],"rawOutput":[{"text":"The spec says X.","type":"text"}],"sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_inner"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_outer"}},"content":{"text":"Plan:","type":"text"},"messageId":"msg_outer_text","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_outer"}},"content":{"text":" do X.","type":"text"},"messageId":"msg_outer_text","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Plan: do X.","type":"text"},"type":"content"}],"rawOutput":[{"text":"Plan: do X.","type":"text"}],"sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_outer"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"capabilities":{"cancel":{}},"description":"Plan the work","sessionId":"agent_outer","sessionUpdate":"subagent_update","state":{"state":"running"},"title":"Plan"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Plan it","type":"text"}],"messageId":"toolu_outer","recipientMessageId":"toolu_outer","recipientSessionId":"agent_outer","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"content":[{"text":"Plan it","type":"text"}],"messageId":"toolu_outer","recipientSessionId":"agent_outer","senderMessageId":"toolu_outer","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"capabilities":{"cancel":{}},"description":"Read the spec","sessionId":"agent_inner","sessionUpdate":"subagent_update","state":{"state":"running"},"title":"Explore"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"content":[{"text":"Read spec.md","type":"text"}],"messageId":"toolu_inner","recipientMessageId":"toolu_inner","recipientSessionId":"agent_inner","senderSessionId":"agent_outer","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"content":[{"text":"Read spec.md","type":"text"}],"messageId":"toolu_inner","recipientSessionId":"agent_inner","senderMessageId":"toolu_inner","senderSessionId":"agent_outer","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_inner"}},"content":[],"kind":"read","locations":[],"name":"Read","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Read File","toolCallId":"toolu_grandchild_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"locations":[{"line":1,"path":"/spec.md"}],"rawInput":{"file_path":"/spec.md"},"sessionUpdate":"tool_call_update","title":"Read /spec.md","toolCallId":"toolu_grandchild_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"content":[{"content":{"text":"```\n1\tspec\n```","type":"text"},"type":"content"}],"rawOutput":"1\tspec","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_grandchild_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"content":[{"text":"The spec says X.","type":"text"}],"messageId":"toolu_inner:reply","recipientSessionId":"agent_outer","senderSessionId":"agent_inner","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"sessionId":"agent_inner","sessionUpdate":"subagent_update","state":{"state":"idle","stopReason":"end_turn"}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_outer"}},"content":{"text":"Plan:","type":"text"},"messageId":"msg_outer_text","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_outer"}},"content":{"text":" do X.","type":"text"},"messageId":"msg_outer_text","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Plan: do X.","type":"text"}],"messageId":"toolu_outer:reply","recipientSessionId":"<session>","senderSessionId":"agent_outer","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionId":"agent_outer","sessionUpdate":"subagent_update","state":{"state":"idle","stopReason":"end_turn"}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,43 +1,51 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"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":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"kind":"think","name":"Task","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Task","toolCallId":"toolu_task"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Explore code"},"sessionUpdate":"tool_call_update","title":"Explore code","toolCallId":"toolu_task"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Explore code","prompt":"Find the parser"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_task"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Find the parser","type":"text"},"type":"content"}],"rawInput":{"description":"Explore code","prompt":"Find the parser","subagent_type":"Explore"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_task"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":{"text":"Looking for","type":"text"},"messageId":"msg_sub_1","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":{"text":" the parser.","type":"text"},"messageId":"msg_sub_1","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":[],"kind":"read","locations":[],"name":"Read","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Read File","toolCallId":"toolu_sub_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"locations":[{"line":1,"path":"/x.ts"}],"rawInput":{"file_path":"/x.ts"},"sessionUpdate":"tool_call_update","title":"Read /x.ts","toolCallId":"toolu_sub_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolResponse":{"elapsedTimeSeconds":3,"subagentType":"Explore"}}},"sessionUpdate":"tool_call_update","status":"in_progress","toolCallId":"toolu_task"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"```\n1\tparser\n```","type":"text"},"type":"content"}],"rawOutput":"1\tparser","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_sub_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"terminal_update","terminalId":"toolu_sub_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":[{"terminalId":"toolu_sub_bash","type":"terminal"}],"kind":"execute","name":"Bash","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Terminal","toolCallId":"toolu_sub_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"command":"rg parser","sessionUpdate":"terminal_update","terminalId":"toolu_sub_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"command":"rg parser"},"sessionUpdate":"tool_call_update","title":"rg parser","toolCallId":"toolu_sub_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"command":"rg parser","description":"Search"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_sub_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"capabilities":{"cancel":{}},"description":"Explore code","sessionId":"agent_1","sessionUpdate":"subagent_update","state":{"state":"running"},"title":"Explore"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Find the parser","type":"text"}],"messageId":"toolu_task","recipientMessageId":"toolu_task","recipientSessionId":"agent_1","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"content":[{"text":"Find the parser","type":"text"}],"messageId":"toolu_task","recipientSessionId":"agent_1","senderMessageId":"toolu_task","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":{"text":"Looking for","type":"text"},"messageId":"msg_sub_1","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":{"text":" the parser.","type":"text"},"messageId":"msg_sub_1","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":[],"kind":"read","locations":[],"name":"Read","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Read File","toolCallId":"toolu_sub_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"locations":[{"line":1,"path":"/x.ts"}],"rawInput":{"file_path":"/x.ts"},"sessionUpdate":"tool_call_update","title":"Read /x.ts","toolCallId":"toolu_sub_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"content":[{"content":{"text":"```\n1\tparser\n```","type":"text"},"type":"content"}],"rawOutput":"1\tparser","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_sub_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"sessionUpdate":"terminal_update","terminalId":"toolu_sub_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":[{"terminalId":"toolu_sub_bash","type":"terminal"}],"kind":"execute","name":"Bash","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Terminal","toolCallId":"toolu_sub_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"command":"rg parser","sessionUpdate":"terminal_update","terminalId":"toolu_sub_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"rawInput":{"command":"rg parser"},"sessionUpdate":"tool_call_update","title":"rg parser","toolCallId":"toolu_sub_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"rawInput":{"command":"rg parser","description":"Search"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_sub_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionId":"agent_1","sessionUpdate":"subagent_update","state":{"state":"requires_action"}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":[{"terminalId":"toolu_sub_bash","type":"terminal"}],"kind":"execute","name":"Bash","rawInput":{"command":"rg parser","description":"Search"},"status":"pending","title":"rg parser","toolCallId":"toolu_sub_bash"},"type":"tool_call"},"title":"rg parser"}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"agent_1","subject":{"toolCall":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":[{"terminalId":"toolu_sub_bash","type":"terminal"}],"kind":"execute","name":"Bash","rawInput":{"command":"rg parser","description":"Search"},"status":"pending","title":"rg parser","toolCallId":"toolu_sub_bash"},"type":"tool_call"},"title":"rg parser"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"output":{"data":"eC50cw=="},"sessionUpdate":"terminal_update","terminalId":"toolu_sub_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"exitStatus":{"exitCode":0},"sessionUpdate":"terminal_update","terminalId":"toolu_sub_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_sub_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":[],"kind":"edit","locations":[],"name":"Edit","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Edit","toolCallId":"toolu_sub_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"locations":[{"path":"<cwd>/x.ts"}],"rawInput":{"file_path":"<cwd>/x.ts"},"sessionUpdate":"tool_call_update","title":"Edit x.ts","toolCallId":"toolu_sub_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"file_path":"<cwd>/x.ts","old_string":"a"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_sub_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"file_path":"<cwd>/x.ts","new_string":"b","old_string":"a"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_sub_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionId":"agent_1","sessionUpdate":"subagent_update","state":{"state":"running"}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"output":{"data":"eC50cw=="},"sessionUpdate":"terminal_update","terminalId":"toolu_sub_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"exitStatus":{"exitCode":0},"sessionUpdate":"terminal_update","terminalId":"toolu_sub_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_sub_bash"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":[],"kind":"edit","locations":[],"name":"Edit","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Edit","toolCallId":"toolu_sub_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"locations":[{"path":"<cwd>/x.ts"}],"rawInput":{"file_path":"<cwd>/x.ts"},"sessionUpdate":"tool_call_update","title":"Edit x.ts","toolCallId":"toolu_sub_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"rawInput":{"file_path":"<cwd>/x.ts","old_string":"a"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_sub_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"rawInput":{"file_path":"<cwd>/x.ts","new_string":"b","old_string":"a"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_sub_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionId":"agent_1","sessionUpdate":"subagent_update","state":{"state":"requires_action"}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"requires_action"}}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"<session>","subject":{"toolCall":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":[],"kind":"edit","locations":[{"path":"<cwd>/x.ts"}],"name":"Edit","rawInput":{"file_path":"<cwd>/x.ts","new_string":"b","old_string":"a"},"status":"pending","title":"Edit x.ts","toolCallId":"toolu_sub_edit"},"type":"tool_call"},"title":"Edit x.ts"}}
{"kind":"request","method":"session/request_permission","payload":{"options":[{"kind":"allow_once","name":"Yes","optionId":"allow-once"},{"kind":"reject_once","name":"No","optionId":"reject"}],"sessionId":"agent_1","subject":{"toolCall":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":[],"kind":"edit","locations":[{"path":"<cwd>/x.ts"}],"name":"Edit","rawInput":{"file_path":"<cwd>/x.ts","new_string":"b","old_string":"a"},"status":"pending","title":"Edit x.ts","toolCallId":"toolu_sub_edit"},"type":"tool_call"},"title":"Edit x.ts"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawOutput":"The file has been updated.","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_sub_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"changes":[{"fileType":"text","operation":"modify","path":"<cwd>/x.ts"}],"patch":{"format":"git_patch","text":"diff --git <cwd>/x.ts <cwd>/x.ts\n--- <cwd>/x.ts\n+++ <cwd>/x.ts\n@@ -1 +1 @@\n-a\n+b\n"},"type":"diff"}],"locations":[{"line":1,"path":"<cwd>/x.ts"}],"sessionUpdate":"tool_call_update","toolCallId":"toolu_sub_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":{"text":"Foun","type":"text"},"messageId":"msg_sub_2","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":{"text":"d it.","type":"text"},"messageId":"msg_sub_2","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"The parser is in x.ts.","type":"text"},"type":"content"}],"rawOutput":[{"text":"The parser is in x.ts.\nagentId: a1 (use SendMessage to continue)","type":"text"}],"sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_task"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"toolResponse":{"status":"completed"}}},"sessionUpdate":"tool_call_update","toolCallId":"toolu_task"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionId":"agent_1","sessionUpdate":"subagent_update","state":{"state":"running"}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"rawOutput":"The file has been updated.","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_sub_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"content":[{"changes":[{"fileType":"text","operation":"modify","path":"<cwd>/x.ts"}],"patch":{"format":"git_patch","text":"diff --git <cwd>/x.ts <cwd>/x.ts\n--- <cwd>/x.ts\n+++ <cwd>/x.ts\n@@ -1 +1 @@\n-a\n+b\n"},"type":"diff"}],"locations":[{"line":1,"path":"<cwd>/x.ts"}],"sessionUpdate":"tool_call_update","toolCallId":"toolu_sub_edit"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":{"text":"Foun","type":"text"},"messageId":"msg_sub_2","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":{"text":"d it.","type":"text"},"messageId":"msg_sub_2","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"The parser is in x.ts.","type":"text"}],"messageId":"toolu_task:reply","recipientSessionId":"<session>","senderSessionId":"agent_1","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionId":"agent_1","sessionUpdate":"subagent_update","state":{"state":"idle","stopReason":"end_turn"}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,21 +1,23 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"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":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[],"kind":"think","name":"Task","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Task","toolCallId":"toolu_transcript"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Summarize"},"sessionUpdate":"tool_call_update","title":"Summarize","toolCallId":"toolu_transcript"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"description":"Summarize","prompt":"Summarize the repo"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_transcript"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Summarize the repo","type":"text"},"type":"content"}],"rawInput":{"description":"Summarize","prompt":"Summarize the repo","subagent_type":"Explore"},"sessionUpdate":"tool_call_update","toolCallId":"toolu_transcript"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"content":{"text":"Look at t","type":"text"},"messageId":"msg_transcript_sub","sessionUpdate":"agent_thought_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"content":{"text":"he layout.","type":"text"},"messageId":"msg_transcript_sub","sessionUpdate":"agent_thought_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"content":{"text":"The repo has ","type":"text"},"messageId":"msg_transcript_sub","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"content":{"text":"two packages.","type":"text"},"messageId":"msg_transcript_sub","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"content":[],"kind":"search","locations":[],"name":"Glob","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Find","toolCallId":"toolu_transcript_glob"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"rawInput":{"pattern":"*"},"sessionUpdate":"tool_call_update","title":"Find `*`","toolCallId":"toolu_transcript_glob"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"a\nb","type":"text"},"type":"content"}],"rawOutput":"a\nb","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_transcript_glob"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"Two packages.","type":"text"},"type":"content"}],"rawOutput":[{"text":"Two packages.","type":"text"}],"sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_transcript"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"capabilities":{"cancel":{}},"description":"Summarize","sessionId":"agent_t","sessionUpdate":"subagent_update","state":{"state":"running"},"title":"Explore"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Summarize the repo","type":"text"}],"messageId":"toolu_transcript","recipientMessageId":"toolu_transcript","recipientSessionId":"agent_t","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"content":[{"text":"Summarize the repo","type":"text"}],"messageId":"toolu_transcript","recipientSessionId":"agent_t","senderMessageId":"toolu_transcript","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"content":{"text":"Look at t","type":"text"},"messageId":"msg_transcript_sub","sessionUpdate":"agent_thought_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"content":{"text":"he layout.","type":"text"},"messageId":"msg_transcript_sub","sessionUpdate":"agent_thought_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"content":{"text":"The repo has ","type":"text"},"messageId":"msg_transcript_sub","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"content":{"text":"two packages.","type":"text"},"messageId":"msg_transcript_sub","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"content":[],"kind":"search","locations":[],"name":"Glob","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Find","toolCallId":"toolu_transcript_glob"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"rawInput":{"pattern":"*"},"sessionUpdate":"tool_call_update","title":"Find `*`","toolCallId":"toolu_transcript_glob"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"content":[{"content":{"text":"a\nb","type":"text"},"type":"content"}],"rawOutput":"a\nb","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_transcript_glob"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Two packages.","type":"text"}],"messageId":"toolu_transcript:reply","recipientSessionId":"<session>","senderSessionId":"agent_t","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionId":"agent_t","sessionUpdate":"subagent_update","state":{"state":"idle","stopReason":"end_turn"}}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"<id-1>","sessionUpdate":"user_message"}}}
{"kind":"response","method":"session/prompt","payload":{"messageId":"<id-1>"}}

View File

@@ -1,4 +1,4 @@
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"providers":{},"session":{"additionalDirectories":{},"delete":{},"fork":{},"mcp":{"http":{},"stdio":{}},"prompt":{"embeddedContext":{},"image":{}}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":"<version>"},"protocolVersion":2}}
{"kind":"response","method":"initialize","payload":{"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}},"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"}],"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":"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>"}}

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