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.
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.
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 "disabled by the user" 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
</tr></table>
</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 "disabled by the user" 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:
"12345"
app.query('/reports', (req, res) => {
res.set('ETag', '"12345"');
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><!DOCTYPE html></code>,
<code><title></code>, and <code><body></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');
// -> 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 -> Content-Disposition: attachment;
filename="user.html"
// after -> 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:
"12345"
app.query('/reports', (req, res) => {
res.set('ETag', '"12345"');
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><!DOCTYPE html></code>,
<code><title></code>, and <code><body></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');
// -> 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 -> Content-Disposition: attachment;
filename="user.html"
// after -> 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>
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>
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.
## 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.
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.
## 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.
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.
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.
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.
### 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>
## Problem
A v1 report's `_meta` belongs to that report, and the agent gives each
report its own `_meta.claudeCode` keys. A v2 client stores a tool call's
`_meta`, and any report that carries `_meta` replaces it
(`tool-calls.mdx`; the migration guide since
agentclientprotocol/agent-client-protocol#2355). So a v2 client kept
only the last report's keys. For example, a result's `{ toolName }`
replaced the denial reason that came before it. On top of that, a
PostToolUse hook stored the whole `tool_response` in `_meta`, which for
a Write is the whole file text.
## Change
All of this is v2 only. No v1 or AIR output changes, and no v1 golden
changes.
- **Stored `_meta` (`src/v2/tool-call-meta.ts`).** The v2 bridge keeps
each tool call's `_meta` the way the client stores it. `claudeCode` keys
merge one by one, and every other key replaces whole, the same model
`ToolCallFieldTracker` uses.
- A report that changes the stored `_meta` carries all of it.
- A report that changes nothing carries no `_meta`, which leaves the
stored value unchanged.
- A permission request's subject always carries the stored `_meta`,
since a client can show the request on its own.
- The tool call's first report starts from nothing.
- **`toolName` becomes `name`.** `claudeCode.toolName` repeats the
tool's name, which v2 has a field for. It becomes the `name` of a tool
call that has none yet, and is left out otherwise. A memory recall gets
a `name` this way. Subagent routing still reads `toolName` on the
agent's v1 reports, before the bridge.
- **The `toolResponse` AIR gets.** A v2 client also keeps `_meta`, so it
gets AIR's trimmed `toolResponse`:
- a hook carries only the `status` and `isAsync` markers, not output the
content already shows;
- a denial carries the SDK message only when it differs from the reason.
## Evidence
- **v2 goldens:** 28 change. Most only lose `toolName`. Hook reports
lose the repeated stdout or file text. Memory recall gains `name`. The
denial keeps its reason type and message. Subagent tool calls and their
permission subjects keep `parentToolUseId`.
- **Live, CLI 2.1.293 on Haiku, v2:** a Write and a Bash command arrived
with `name` on their first report and no `_meta` at all.
- **Tests:** 7 unit tests for the stored `_meta`.
- **Mutation checks:** removing any of these 9 rules fails at least one
test:
- naming once;
- leaving `toolName` out;
- merging with earlier `claudeCode` keys, which also fails the
`subagent-late-child-update` golden;
- keeping other keys;
- omitting unchanged `_meta`;
- starting fresh;
- the subject's stored `_meta`;
- the bridge wiring;
- the trimmed `toolResponse`.
Forgetting a closed session's tool calls is memory cleanup that no test
observes, like `V2Terminals.forget`. A limit of 2048 tool calls also
bounds the memory.
- `npm run check`, build and all 2820 tests pass.
## Problem
The v1 schema says a forked session is active once the agent responds,
and can be prompted without `session/load` or `session/resume`
(agentclientprotocol/agent-client-protocol#2359). `session/fork` only
copied the transcript, so prompting the new id failed with "Session not
found" until the client loaded or resumed it.
## Change
- **The fork starts the new session the way a resume does.** It starts
from the copied transcript, with the model and the permission mode that
transcript records, and with the request's `cwd`, MCP servers,
additional directories and `_meta`. The response carries the session's
`modes` and `configOptions`, and the available commands follow it, as
for `session/new`.
- **A client that loads or resumes after a fork keeps working.** A later
`session/load` replays the history into the running session, and a later
`session/resume` returns it. Neither starts a second Claude Code
process. A load or resume with other settings restarts the session, as
for any loaded session. The fingerprint that decides this ignores the
AIR fork point in `_meta`.
- **Cost:** each fork now starts a Claude Code process. That added about
0.65 s to the fork response in the live run.
- **v2:** the v2 fork used to resume the forked session itself. It now
uses the session that the v1 fork starts, so the v2 bridge drops its own
resume.
- **AIR docs:** `docs/air-extensions.md` now says, under the session
fork point, what a load or resume after a fork does.
## Evidence
**Live, CLI 2.1.293 on Haiku, over v1:**
1. A new session answered "Remember the codeword PELICAN" with `OK`.
2. `session/fork` returned the fork with mode `auto` and the `mode`,
`model` and `effort` config options.
3. Prompting the fork right away returned `PELICAN`, with no load or
resume in between.
4. A `session/load` of the fork then replayed its history and was ready
in 4 ms. The timing log shows one session start for the fork and none
for the load, and a further prompt still answered.
**Tests:**
- New: the fork can be prompted at once, and its response carries modes
and config options; a later resume and load reuse the session; and the
fork fails when its session can't start.
- The existing fork tests now also check that the session starts from
the fork, with the request's additional directories.
- The v2 fork tests pass unchanged.
- **Mutation checks:** removing the start, the response's `modes`, or
the additional directories each fails a test.
- `npm run check`, build and all 2813 tests pass, with no golden
changes.
## Summary
When the user approves a plan through ExitPlanMode, a v2 client now gets
it as the session's markdown plan. The update uses the draft's unstable
plan operations:
```json
{ "sessionUpdate": "plan_update", "plan": { "type": "markdown", "planId": "plan-mode", "content": "# Plan…" } }
```
Until now a v2 client saw the plan only in the "Approve Plan" tool call
and its permission request.
## Behavior
- **Its own plan id.** The approved plan has the id `plan-mode`, and the
TodoWrite checklist keeps `plan`, so the client shows both. Claude Code
keeps one plan file per session and rewrites it on a later plan mode, so
each approval replaces the plan before it. Nothing removes it.
- **A normal approval** is a successful ExitPlanMode result. The plan
follows the tool call's result, live and on replay.
- **Live:** the plan comes from the tool's structured output, which
includes edits the user made to the plan.
- **Replay:** the SDK's `getSessionMessages` drops the structured
output, but the transcript keeps the `plan` that Claude Code adds to the
tool's input.
- **An approval that clears the context** ends the tool with an error
result. The restart into the fresh context publishes that plan, after
the new mode and config options and before the fresh context runs. It
does so only once that context exists, so a cancel or a failed restart
leaves no plan behind that was never carried out.
- **Not published:** a rejected plan, a subagent's plan
(`parent_tool_use_id` or `isAgent`), a plan still waiting for its team
lead, and an empty plan. Plans are trimmed.
- **Who gets it.** The agent sends plan operations only to a client that
advertises v1's draft `plan` capability, as the v1 draft requires. The
v2 bridge advertises it for every v2 client and passes `plan_update` and
`plan_removed` through unchanged, since the shapes match. No v1 client
advertises `plan` today, so v1 behavior doesn't change.
## Replay limits
- **After a "clear context" approval, replay doesn't show that plan.**
It reads the transcript from before the clear, where the approval is the
deny, so it shows the plan approved before that, or none. Replay never
reaches the conversation after a clear, so this is part of an existing
gap.
- **A plan approved before a compaction boundary isn't replayed**, like
the rest of the history before it.
- **Replay can't apply the subagent and team-lead checks**, since they
come from the structured output that `getSessionMessages` drops. A
subagent's transcript still carries its `parent_tool_use_id`, so it's
skipped.
## Evidence
**Live, CLI 2.1.293 on Haiku, through the adapter on v2 in plan mode:**
- **Normal approval:** the plan arrived right after "Exited Plan Mode",
and resuming with replay republished it.
- **"Clear context":** the plan arrived after the switch to auto mode,
before the fresh context's first tool call.
- **Raw SDK probe:** the structured result carries `plan`, `isAgent` and
`filePath`, and the transcript's tool use keeps `plan` and
`planFilePath`.
**Tests and checks**
- **Result path:** unit tests for each rule and for the order. They
include structured output versus input, input-only as on replay, and
structured output with no plan.
- **Restart path:** coordinator tests check that the plan comes after
the session state and before the continuation, that a client without the
capability gets none, and that a failed restart publishes none.
- **Agent:** a client that advertises `plan` gets the restart's plan,
and one that doesn't gets nothing.
- **v2:** a replay test and translation tests. The v2
`exit-plan-approve` golden gains the plan update. `exit-plan-reject` and
every v1 golden are unchanged.
- **Mutation checks:** removing any rule on either path fails at least
one test. That covers both capability checks, the agent's capability
wiring, success only, structured output first, the order, trimming, and
the `plan_removed` pass-through.
- `npm run check`, build and all 2810 tests pass.
## Problem
After a cancel, the consumer drops the main conversation's consolidated
`assistant`/`user` messages until Claude Code starts a model request
that serves none of the cancelled prompts (#1309). `stream_event` frames
skipped that check. Claude Code always streams, so when it had already
taken an abandoned prompt off its queue, the prompt's answer still
streamed to the client as deltas. Only the consolidated copy was
dropped.
That output arrives after the cancel has been reported: on v1 after the
turn's `cancelled` response, and on v2 after `idle cancelled`, outside
any cycle. Both protocol versions require an agent's updates to come
before that report. Streamed tool calls leaked the same way.
#1309's tests missed this because they scripted only consolidated
messages.
## Change
- **After the client has been told the stopped work ended**, while the
window is still open, streamed text, thinking and tool calls are dropped
like the consolidated messages they belong to. On v1 that report is the
turn's prompt response (no active turn). On v2 it's the idle state (no
open foreground work). The v2 signal also covers cycles that Claude Code
starts on its own and turns that v1 holds for their subagents.
- **Before that report**, what the interrupt flushes still streams, as
it does today. Both drafts allow updates between the cancel and the
cancelled report, and the text is in Claude Code's transcript, so a
reload shows it anyway. Existing tests pin this: v2's queued-prompt
cancel, a held turn cancelled mid-follow-up, and a cancel that arrives
during a buffered backlog.
- **A prompt that is merely queued no longer keeps the window open.**
#1309 held the window while any queued prompt was unsettled. So a
follow-up that Claude Code ran ahead of a prompt sent after the cancel
lost its consolidated copy, and with this change it would have lost its
streamed copy too. Now a request holds the window only when:
- the stopped turn is still unsettled;
- an abandoned prompt's result is still expected;
- the request names a cancelled prompt; or
- the request names an unsettled prompt. That covers one that arrived
while the cancel was still awaiting, before the cancel records it.
- **Per-block drop latch.** Where the consolidated message delivers the
text, a stream that loses a delta keeps dropping until its streamed-text
record resets: at the next message, or when a consolidated message
consumes the record. If a window closes mid-block (an older CLI's late
prompt echo), the record holds nothing, so the consolidated message
delivers the whole block once. Without the latch the record would hold
only the tail, which isn't a prefix of the text, and the client would
get the tail and then the whole text again. A client that gets subagent
text only streamed has no consolidated copy to fall back on, so its
subagent streams don't latch.
- Bookkeeping for dropped events (message id, usage, compaction
heartbeat) is unchanged.
## Evidence
**Tests**
- The "output after a cancel" suite now streams every answer the way
Claude Code does, with Claude Code's follow-up in two chunks. Without
the fix, 9 of its tests leak the streamed copy.
- New v1 tests:
- a follow-up that runs ahead of a prompt sent after the cancel;
- a prompt sent during the cancel whose request arrives before the
cancel abandons it;
- an echo in the middle of a two-block answer;
- a subagent stream for a client that gets subagent text only streamed.
- New v2 tests:
- an abandoned prompt's streamed answer;
- what the interrupt flushes of a cycle that Claude Code started;
- a held turn's follow-up that the cancel stopped before it showed
anything.
- **Mutation checks:** each of these fails at least one test:
- removing any of the window's four rules, or restoring #1309's hold for
any queued prompt;
- removing the drop;
- dropping the flushed tail before the report too;
- using the v1 signal on v2;
- removing the latch, either of its record resets, or the subagent
condition.
The exception is clearing the latch when a subagent finishes. That's
memory cleanup that no test can observe, like the
`streamedBlocksByParent.delete` beside it.
- `npm run check`, build and all 2796 tests pass, with no golden
changes.
**Live, CLI 2.1.293 on Haiku, v1 and v2**
- A plain cancel mid-answer, and cancels with a background command
finishing during or after the cancelled turn.
- Nothing of the cancelled turn arrived after its report.
- Claude Code's follow-up after the cancel streamed in full. On v2 it
came in its own `running` … `idle end_turn` cycle.
- An abandoned prompt that Claude Code had already dequeued is a narrow
race through the adapter, so I didn't reproduce it live. The tests
follow the order a direct CLI probe recorded for #1309.
## Not changed
- A dropped prompt that runs a tool can still surface it after the
cancel, through `canUseTool` (tool call plus permission request) and the
PreToolUse preview hook. Neither checks the cancel. A check there would
read the consumer's state, which lags the CLI's control channel, so this
needs its own design.
- On CLIs without `request_marker_lists_v1`, the window still lasts
until the next prompt's echo. Claude Code's own follow-ups in that
window now lose their streamed copy as well as the consolidated one. The
bundled CLI has the marker.
A Bash call that sends its command to the background returns as soon as
the command detaches, and its tool result carried `terminal_exit`. So a
client that renders terminals, whether Zed's terminal extension or a v2
display terminal, showed the command's terminal as exited, with no code,
at launch, while the command ran on.
## What changes
For a client that renders terminals and isn't AIR:
- **At launch:** the tool result of a command in the background leaves
the exit out (`withRunningTerminal`). The tool call still completes; the
v2 draft keeps "Terminal exit and tool call status are independent".
- **When the command's task ends:** the consumer sends `terminal_exit`.
It does so at the task's `task_notification`, or at its terminal
`task_updated` if that comes first. The update is routed like the tool
call, so a subagent's command ends in the subagent's session.
- **No exit code.** Claude Code names the code only in the
notification's summary text ("…failed with exit code 3"), which the
adapter doesn't parse. `completed` doesn't mean 0 either: Claude Code
accepts some non-zero codes as a success, such as grep's 1.
The terminal still exits at the tool result, as before, in four cases:
- **A fast command** whose task ended before its tool result.
- **A task that started after the tool result.** Claude Code sends
`task_started` first, but the existing `bash-background` scenario
scripts the reverse, so its goldens don't change.
- **AIR,** which shows the command through its async tasks.
- **Replay,** where the command from before the reload no longer runs,
and the structured tool result isn't available anyway.
## Evidence
- **Live, CLI 2.1.293, through the built adapter over ACP:**
- **v2:** the terminal stays running when the Bash call completes and
the turn goes idle. `terminal_update` with `exitStatus: {}` arrives when
`sleep 5` finishes, before Claude Code's follow-up.
- **v1 with `terminal_output`:** no exit at the Bash result;
`terminal_exit` with code `null` arrives when the command finishes.
- **Recorded, CLI 2.1.293:**
- the order is `task_started` (`local_bash`, `is_backgrounded: true`),
then the Bash result with `tool_use_result.backgroundTaskId`, then
`task_updated` and `task_notification` when the command ends;
- neither event carries a structured exit code.
- **Tests:**
- v2: the terminal runs until the task ends, with one exit after the
turn's `idle`; and a task that ended before the result exits at the
result.
- v1 with Zed's terminal extension: the exit comes after the prompt's
answer. AIR keeps its exit at the result.
- Unit tests for `withRunningTerminal`, including dropping a `_meta`
that would be left empty, since an empty `_meta` would clear what a v2
client stores.
- **Mutations:** removing the strip, the end-of-task exit, the liveness
guard, or the AIR guard each fails a test.
- `npm run check`, build and the full suite pass, with no golden file
changes.
A follow-up to #1310, from its review, which found no regressions but
two wrong answers. Both come from one choice in #1310: a request from an
agent without a `liveBackgroundTasks` record put v2's foreground work in
`requires_action`.
**When an agent has no record:**
- **A teammate:** it never gets one.
- **A settled subagent:** settling deletes its record, and settling
didn't re-check open requests.
- **A foreground subagent nested in a background one:** Claude Code
leaves it out of `background_tasks_changed`, so the level reconciliation
prunes it after two activations.
The main conversation waits on none of them. A background subagent's
request that the consumer read before its `task_started` also flickered
from `requires_action` to `running`.
## What changes
- **An agent without a record no longer blocks the foreground work.** A
subagent the main conversation waits on has a record by the time it
asks: its `task_started` comes first on the wire, and nothing prunes it
while the main turn waits. When the consumer reads the request first,
the `task_started` checks again, so `requires_action` comes slightly
later instead of a flicker for background subagents.
- **Settling a task re-checks** the open requests.
- **The comment on `nativeSubagentParentByToolUseId`** no longer claims
`parent_task_id` is unread.
## Tests
- **Replaced:** "stops waiting for a request once its subagent turns out
to run in the background". The new test checks that a request read
before its `task_started` doesn't block, then blocks once the
`task_started` says foreground.
- **New:** a foreground subagent's open request stops blocking once
Claude Code stops that subagent.
- **Mutations:** restoring "no record blocks" fails both tests; dropping
the re-check at settle fails the second.
- `npm run check` and the full suite pass, with no golden file changes.
v2's foreground work reported `requires_action` whenever any permission
request or question in the session was open, including a background
subagent's. The draft keeps `requires_action` for foreground work that
is blocked on user action, and a background subagent asks without
blocking the main conversation. So a v2 client saw a running turn as
waiting on the user while it kept working.
## What changes
A request blocks the foreground work when it is:
- the main conversation's own request, or
- the request of a subagent that runs in the foreground of the main
conversation, so the tool call that spawned it waits on it. This applies
up a chain of such subagents.
A request from a subagent in the background, or from one nested inside
it, no longer counts.
- **Where each task runs.** The adapter records this per task:
- from `task_started.is_backgrounded` and `parent_task_id`;
- from a later `task_updated` patch that moves the task to the
background;
- Claude Code always resumes a subagent in the background.
- **Unknown subagents.** A subagent without a record still counts,
because its request can arrive before its `task_started`. Registering a
task, or moving one to the background, checks the open requests again.
- **Requests that name no agent** still count: MCP elicitations and
dialogs.
- **Steering is unchanged.** It still treats any open request as
blocking. `Session.pendingUserInputCount` becomes
`Session.openUserInputs`: the open requests, each with the subagent that
asked.
- **v1 is unchanged.**
## Evidence
- **Live, CLI 2.1.293 via the SDK with a logging `canUseTool`:**
- `task_started` carries `is_backgrounded: true` for a background
subagent and `false` for a foreground one.
- A subagent's `canUseTool` call carries `agentID` equal to its
`task_started.task_id`.
- `task_started` arrived before the subagent's request in both cases.
- `parent_task_id` for nested subagents was not checked live; it's
documented in the SDK types and covered by a test.
- **6 new v2 tests:**
- a background subagent's permission request leaves the turn `running`,
and so does its question;
- a foreground subagent's request reports `requires_action`;
- a foreground subagent launched by a background one doesn't block;
- a request that arrives before its subagent's `task_started` stops
blocking once the subagent turns out to run in the background;
- a request stops blocking once Claude Code moves its subagent to the
background.
- **Mutation checks.** Removing each rule in turn fails at least one
test. That includes passing the subagent through for permission requests
and for questions, the recheck at registration and at `task_updated`,
and the walk up the chain.
- `npm run check`, build and the full suite pass, with no golden file
changes. Two resume tests now expect the resumed task to be recorded as
backgrounded.
After a cancel, Claude Code ends the interrupted turn with an aborted
result, then keeps working through its queue, and reports `idle` once at
the end. The queue can hold a prompt it had already taken off it, then
its own follow-ups, such as the summary of a background command that
finished. The adapter settled a cancelled turn only at that final
`idle`, and dropped everything in between until the next prompt
activated. As a result:
- **v1:** a cancelled `session/prompt` waited for any follow-up queued
behind it to finish.
- **v2:** a follow-up after a cancel streamed as text with no `running`
or `idle`.
This PR ends the cancel where Claude Code's own messages say the stopped
work ends.
## What changes
- **Where a cancelled turn settles.** It now settles at the result of
the work the interrupt stopped: its own result, or, for a turn held open
for its subagents, its follow-up's aborted result.
- The `idle`, the next echo's hand-off and the force-cancel backstop
still settle a turn whose result never comes.
- That result owes its trailing `idle` like any settling result, a
steered turn's included.
- **When the cancel ends.** The cancel records the prompts it stopped:
the unsettled turns and their steers when it arrives, and whatever it
abandons when it sweeps the queue.
- With `request_marker_lists_v1`, every `status: requesting` frame lists
the prompts its model request serves. A cycle Claude Code starts on its
own lists none.
- The cancel ends at the first request that serves none of the recorded
prompts, once no turn is unsettled and no orphan result is outstanding.
Compaction updates resume there too.
- A prompt Claude Code had already dequeued names itself, so its answer
stays dropped even when the interrupt receipt leaves it out.
- Without the capability, the cancel ends at the next activation, as
before.
- **Abandoned prompts' results.** A result that names a cancelled prompt
never promotes the next queued prompt, so a prompt sent after the cancel
can't take an abandoned prompt's result.
- **Held turns.** Since CLI 2.1.270, Claude Code reports `running` for
as long as background agents live. So with request markers, a held
turn's follow-up counts as live only when a model request has started
since the turn's result. A cancel while only the subagent runs settles
the held turn at once.
- **v2: aborted cycles.** A cycle whose result is aborted
(`terminal_reason` `aborted_streaming` or `aborted_tools`) ends
`cancelled`.
## Evidence
**What CLI 2.1.293 sends.** I recorded the stream live with a probe that
drives the SDK the way the adapter does. After an interrupt:
1. The interrupted turn's aborted result comes first.
2. Next, any prompt Claude Code had already taken off its queue runs.
Its request names it.
3. Then its own follow-ups run. Their requests name no prompt.
4. Finally, one `idle`.
The interrupt stops background agents, but not background commands.
**An audit of the bundled CLI** found exactly one source of `status:
requesting` frames: one per main query-loop iteration. Subagents,
compaction, side queries, hooks and retries emit none, or only a named
one.
**The adapter, end to end over ACP against the live CLI**, this branch
against `main`:
| Scenario | `main` | This PR |
| --- | --- | --- |
| v1: a background command finishes while the turn the user cancels
streams | `session/prompt` answered 14.1 s after the cancel | answered 4
ms after the cancel; the follow-up's text arrives after |
| v2: same | follow-up text with no `running`; `idle cancelled` 14.8 s
after the cancel | `idle cancelled` 10 ms after the cancel, then
`running`, the follow-up, `idle end_turn` |
| v2: a background command finishes after the cancelled turn | `idle
cancelled`, then text with no `running` or `idle` | `idle cancelled`,
then `running`, the follow-up, `idle end_turn` |
| v2: cancel while idle, then a background command finishes | text with
no `running` or `idle` | `running`, the follow-up, `idle end_turn` |
| v1: cancel while a background subagent holds the prompt open | (not
compared) | answered 2 ms after the cancel |
## Tests
- **20 new tests, scripted on the recorded orders.**
- v1 suite "output after a cancel" (12): a cancelled turn and a cancel
while idle; a dequeued prompt the receipt leaves out; a steer; a prompt
that Claude Code folds into its own follow-up; a prompt sent while the
cancel is still in progress; a repeated cancel; trailing idles after a
cancelled turn and after a steered one; older CLIs without request
markers.
- Held turns (3): a cancelled held turn settles at its follow-up's
aborted result, not at a placeholder result, and at once when only its
subagent runs.
- v2 (5): the follow-up after a cancelled turn as its own cycle; a
cancel while idle; a later cycle after a cancelled one; a cycle Claude
Code started before the cancel arrived; a follow-up's compaction.
- **Mutation checks.** Removing each rule in turn fails at least one
test. Two exceptions:
- Clearing the record at activation can't be observed.
- Holding for any unsettled turn, not only a recorded one, can't be told
apart from activation.
- `npm run check`, build and the full suite pass, with no golden file
changes.
## Review
The design went through two rounds of independent review.
- **The first attempt, rejected.** It judged "the stopped work is over"
from the adapter's own bookkeeping. Review found that the interrupt
stops what Claude Code is running, which the adapter may not have seen
yet. That attempt is not in this PR.
- **This design.** A second round audited the CLI's signals and the
state machine. This PR fixes what it found:
- a prompt sent after a cancel could take an abandoned prompt's result;
- a held turn could settle at a placeholder result;
- the window could close while an abandoned prompt that Claude Code
folds into its own follow-up was still due;
- a prompt sent during the cancel's awaits wasn't recorded;
- a held turn's follow-up was misread as live on CLI 2.1.270+.
## Known limitations
- **The cancelled turn's last streamed text still arrives after the
cancel**, as on `main`: the cancel never held streamed text back.
- **If the cancel lands just after Claude Code started its own
follow-up**, the interrupt stops that follow-up, and its partial output
gets through. On v2 it ends `cancelled`.
- **Trailing-idle debt can be over-counted by one in a drain run.** The
next `running` transition sweeps it, as before.
Stacked on #1304, which this targets; it retargets to `main` once #1304
merges. Together they replace #1303.
## What a v2 client gets now
**`idle` at a turn's result,** also when v1 holds the turn's answer for
the background subagents it spawned (`Turn.deferredSettle`).
- The subagents' output and permission requests arrive while the session
is idle. The draft allows that: background activity may send updates
while idle, and they don't change the state.
- A background request while idle no longer shows `requires_action`,
which the draft reserves for foreground work.
- The hold itself is unchanged. It now only delays v1's answer, with no
v2 branch in the hold logic.
**Model cycles that Claude Code starts on its own** are foreground work:
the follow-up after a background subagent or command finished, or its
answer to a message from another session.
- A cycle reports `running` before its first output, `requires_action`
while it waits, then `idle` with its own stop reason, usage and
`_meta.quota`.
- An error result gives `idle` with the `error` stop reason, and a
cancel gives `idle cancelled`.
- Before this, such a cycle streamed while v2 said `idle`.
**Take-overs:** a prompt that Claude Code takes in during a cycle takes
it over, with no new `running` and no `idle` in between. So do a steer
that resumes a held turn, and the continuation of an accepted
clear-context plan. In both, the final `idle` carries the turn's usage,
not just the last cycle's.
v1 and AIR clients are unchanged.
## How
All of this builds on #1304's `ForegroundEvents`:
- **A turn's work ends at its result:** `endForeground` in the defer
branch of `settleOrDefer`, and where the steer idle lane re-applies a
hold.
- **A cycle starts** (`startAutonomousCycle`) at model output of the
main conversation while no work runs.
- Subagent output doesn't count.
- Without command lifecycles, output while a prompt is queued stays that
prompt's, since its output can come before its echo. With lifecycles
(`msg_lifecycle_v1`), a prompt Claude Code hasn't started has no output
yet, so a cycle starts. This narrows the gap that #1303 left.
- **A cycle ends:**
- at a result no turn took, except a placeholder result (`num_turns:
0`);
- with `cancelled` at the diagnostic of an ExitPlanMode answer, as a
turn does, unless a held turn continues the accepted plan;
- at an SDK `idle` that no earlier result owes;
- when the stream ends or fails;
- before `session/close` answers.
- **Starting work while a request is open** reports `requires_action` at
once.
- **The cycle's stop reason** comes from its result, as for a turn
(`autonomousCycleEnd`). Its `_meta.quota` uses the same builder as a
turn's (`quotaMeta`, extracted from `turnQuotaMeta`).
## Checks
- `npm run check`, `npm run build` and all 2756 tests pass. No golden
file changes against #1304: no recorded scenario has a held turn or
output while no turn is active.
- New tests:
- **14 v2 tests:** #1303's eleven, plus a held turn reporting `idle` at
its result with its follow-up as a cycle and a background request while
idle, output before a queued prompt that Claude Code reports it hasn't
started, and `session/close` during a cycle;
- **agent:** a steer resuming a held turn's work; the follow-up that
ExitPlanMode's "keep planning" interrupts ending `cancelled`; the
clear-context continuation taking the cycle over, with the turn's usage;
- **turn events:** work that starts while a request is open waits at
once.
- I made 20 mutations, one per rule above, and each failed a test. The
take-overs for a steer and for the clear-context restart first survived,
since a cycle stood in for the turn with the same states. The tests now
check the usage of the final `idle` too.
## Not covered
- No recorded scenario has such a cycle. The start signal (the first
main-thread output) is chosen from the agent's side, not confirmed
against a live CLI.
- After a cancel, the agent drops main-thread output until the next
prompt (`session.cancelled` stays set). So a follow-up that Claude Code
runs after a cancel is still lost, for v1 and v2. That predates this PR,
and I'll fix it separately.
The first of two PRs that replace #1303, after a design review. This one
restructures how the agent reports v2's state, with two small changes in
behavior (below). The next PR makes v2 report `idle` at a turn's result
and adds model cycles that Claude Code starts on its own.
## Why
The agent reported each turn's v2 states (`running`, `requires_action`,
`idle`) through the prompt's `TurnEvents`, the same events that answer
the prompt. That ties v2's state to when **v1** gets its answer. The
clearest case is the hold (`Turn.deferredSettle`): v1 holds a turn open
while the background subagents it spawned run, because a v1 client stops
listening at the prompt response. So v2 stays `running` too.
Making v2 report `idle` at the turn's result in that design would need a
v2 branch inside the hold logic. That's the most intricate code in the
adapter: steering, cancel, the echo hand-off, ExitPlanMode, idle debts.
And a cycle that Claude Code starts on its own would need a second
source of v2 state beside the turn's (#1303).
## What changes
- **`TurnEvents` only answer the prompt:** `inserted`, `ended`,
`failed`. v1 answers at `ended` or `failed`. v2 answers at `inserted`,
and uses `ended`/`failed` only to reject a prompt before insertion.
- **New `ForegroundEvents`** (`src/turn-events.ts`): the session's
foreground work, meaning what a client would wait for before sending the
next prompt. Events: `running`, `awaitingUser`, `ended`, `failed`.
- Only the v2 surface supplies them, through the new `foreground`
option. They map to `state_update` in `v2Foreground`.
- A prompt's turn is the foreground work from its insertion until it
ends, exactly as before. So `idle` still waits for a held turn's
subagents; the next PR changes that.
- **The work is keyed by the session id the client knows,** not stored
on a `Session`. A clear-context restart (an accepted ExitPlanMode with a
fresh context) replaces the Claude Code session behind that id and moves
the running turn to it. Work stored on the old `Session` would stay
behind.
- **Requests report through the work** (`syncAwaitingUser`), not through
the active turn.
## Observable changes on v2
1. **The `session/prompt` response now comes before `running`, not after
it.**
- The 38 v2 golden files change by exactly that, and nothing else; I
checked every file with a script.
- The old order was not guaranteed: the SDK's `notify` writes after
`waitUntilInitialized(...).then(...)`, and the response is written once
the handler's promise resolves. The old code won that race by sending
`running` before resolving.
- The draft lets clients receive the updates before or after the
response.
2. **A request that Claude Code opens for a prompt it started, before
the prompt's echo, now reports `requires_action` when it opens,** rather
than at the echo. The prompt's work is already running then.
v1 and AIR clients are unchanged.
## Checks
- `npm run check`, `npm run build` and all 2739 tests pass.
- Every v2 state test in `acp-v2.test.ts` passes unchanged.
- `turn-events.test.ts` now reads the waiting events from the
foreground.
- New tests:
- a request opened before the echo;
- the work staying with the session id across a clear-context restart,
with a permission request in the continuation.
- I made six mutations and each failed a test: no `running` at
insertion, no end on `resolve`, no end on `reject`, waiting never
reported, waiting only for the active turn, and the work tied to the
`Session` object.
IDEs can pass their own Claude hooks through
`_meta.claudeCode.options.settings.hooks` on `session/new` and
`session/load`. Before this change, any session settings replaced
`CLAUDE_MODEL_CONFIG` entirely, so adding a single hook dropped the
deployment's `modelOverrides` / `availableModels`. The two are now
merged, and the session settings win per key.
The wrapper does not merge hooks itself: Claude Code already runs the
hooks of every settings tier, so user and project hooks keep running
next to the session ones. Hooks stay scoped to their session, and a
`session/load` with different settings recreates the query, which
replaces or removes them.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#1289 made a v2 client get `cancelled` for a run that Claude Code
aborted, when Claude Code reports it as an error result with
`non_execution_kind`. Claude Code has a second way to report an aborted
command: a **successful** Bash result whose structured output says
`interrupted: true`. A v2 client got `completed` for that. With this PR
it gets `cancelled` too, so the rule is the same however Claude Code
reports the abort.
## When Claude Code sends that result
I read the CLI that SDK 0.3.293 bundles. I didn't reproduce these cases
live.
| What stops the command | What the adapter receives | v2 before | v2
after |
| --- | --- | --- | --- |
| The client cancels (`session/cancel`) | an error result,
`non_execution_kind: "interrupted"` | `cancelled` (#1289) | same |
| A timeout | an error, "Command timed out after …", **not**
`interrupted` (or the command is backgrounded) | `failed` | same |
| The output limit or a replaced output file | an error with `Exit code
137`, not `interrupted` | `failed` | same |
| A `now`-priority message such as `_session/steering`, landing just
before the command starts, or while the output limit kills it | a
successful result, `interrupted: true` | `completed` | **`cancelled`** |
A successful `interrupted` result needs the abort reason `interrupt`,
which a `now`-priority message causes. That abort doesn't kill a running
command, so this result only comes from the races in the last row.
Clients will rarely see it. The point of the change is that v2's status
follows what Claude Code reports, however it reports it.
v1 and AIR clients are unchanged: `completed` with no exit code, and AIR
still gets exit code 1.
## How
- `AcpToolCallRenderer.resultStatus` takes the command's `interrupted`
fact alongside `nonExecution`.
- `result()` used to call `resultFields`, which drops the command facts.
It now computes the facts once (`resultFacts`) and builds the fields
from them (`fieldsOfResult`). That lets it pass `interrupted` without
adding a field to `RenderedResult`, which the public
`toolUpdateFromToolResult` returns.
- The agent's checks for a finished tool call already count `cancelled`
(`isFinalToolCallStatus`, #1289).
## Checks
- `npm run check`, `npm run build` and all 2730 tests pass. No golden
file changes: no recorded scenario has an interrupted command.
- New tests:
- **renderer:** an interrupted command on v2, v1 and AIR, and one that
isn't interrupted on v2;
- **v2 surface:** a v2 client receives `cancelled`, and the turn still
ends `end_turn`.
- I made four mutations and each failed a test: the rule ignores
`interrupted`, every client gets `cancelled`, `result()` doesn't pass
the fact, and `result()` inverts it.
v2 has a `cancelled` tool call status ("the tool call was cancelled
before it completed"), which v1 doesn't. #1289 started using it for a
run that Claude Code aborted. This PR uses it for the other tool call
that never ran: one that Claude dropped before running it.
## What changes
When a turn ends with `end_turn` and a streamed `tool_use` never reached
a complete assistant message, Claude dropped that tool call before it
ran, for example when it switched to a steering message. The agent
closes it at the end of the turn with "Claude stopped this tool call
before it ran." The turn itself still succeeds.
| Case | v2 client | v1 and AIR clients |
| --- | --- | --- |
| Dropped before it ran | `cancelled` (was `failed`) | `failed`, as
before |
| Ran, but Claude Code returned no result | `failed`, as before, and the
turn fails with `incomplete_tool_call` | same |
The content text is unchanged in both cases.
## How
- **The renderer builds both closing reports**
(`AcpToolCallRenderer.unfinished`), and picks the status from
`ClientCapabilities.v2`, as `resultStatus` does since #1289. Before, the
agent built the report inline with a hardcoded `failed`.
- **The agent** passes whether the tool ran. Its closing step
(`failToolCall`, now `closeUnfinished`) already removes the call from
all of its tracking itself, so no "is this status final" check needed
changing.
## Checks
- `npm run check`, `npm run build` and all 2728 tests pass. No golden
file changes: no recorded scenario drops a tool call.
- New tests:
- **renderer:** dropped and ran-without-result, on v2, v1 and AIR;
- **v2 surface:** a v2 client receives `pending` then `cancelled` for a
dropped tool call, and the turn ends `end_turn`.
- The existing v1 tests in `incomplete-tools.test.ts` are unchanged and
still pass.
- I made five mutations and each failed a test: the renderer never
cancels, the renderer cancels a tool that ran, the agent marks a dropped
call as having run, the agent marks a no-result call as dropped, and the
agent's renderer ignores the client's capabilities.
A v2 diff can't hold the snippet of an Edit's input (it needs line
numbers and the operation), so on v2 an auto-approved Edit or Write
showed its change only once the PostToolUse hook reported it, after the
tool had already run. Only an asked one showed its approval patch first.
## What changes, v2 only
The tool call shows the exact patch before the tool runs, auto-approved
or not.
## How
- **A PreToolUse hook for Edit and Write** (`fileChangePreviewHook`),
registered only on v2 connections. Claude Code runs the tool only after
the hook returns, so the file is still unchanged when the hook reads it.
That's the same guarantee the approval patch in `canUseTool` relies on.
The hook sends the same patch (`previewPatchContent`):
- **Stream already showed the tool call:** a `tool_call_update` through
the session's stream router, pinned like an approval patch.
- **Hook came first:** the SDK dispatches hook callbacks as soon as it
reads them, ahead of the message queue the agent consumes. The hook then
emits the tool call with the patch via `ensureToolCallEmitted`, the
permission flow's fix for the same race (#851).
- **Subagent tools** are attributed exactly as in `canUseTool`. That
rule moves into `toolUseOwner`, which both use, with no behavior change.
- **Safe failure:** the hook makes no permission decision, and any
failure only leaves the preview out.
- **v1 and AIR** register no PreToolUse hook, so Claude Code waits on
nothing new there and their output is unchanged.
I first tried hanging the preview on the per-tool-use hook registry that
PostToolUse uses. That fails when the hook comes before the stream, so
the hook lives on the agent instead.
## Golden files
The scenario harness now runs PreToolUse before each tool, as Claude
Code does, and applies each hook's `matcher`. The v2 golden files of the
four file-change scenarios change:
- **`write-existing`** (auto-approved): the patch now arrives before the
tool completes, and the final hook report no longer repeats it, because
the tracker sees no change.
- **`write-new`, `edit-with-permission`, `edit-rejected`:** the tool
call gets the patch just before the permission request, which carries
the same patch.
No v1, AIR or plain golden file changes.
## Checks
- `npm run check`, `npm run build` and all 2497 tests pass.
- A new v2 test runs an auto-approved Edit with the hook both after and
before the streamed tool use. It checks that the patch is computed from
the file before the edit, arrives before the result, and isn't replaced.
- A new test checks that only a v2 session registers the hook, with the
`Edit|Write` matcher.
- Mutations that fail tests: no hook on v2, the hook on v1 too, the
hook-first path doing nothing, the already-shown path sending nothing,
and Edit only.
- Not pinned by any test: the pin itself. On v2 nothing later in the
stream carries content for an Edit or Write, so it's only there for
consistency with the approval patch.
## Follow-up
AIR has the same gap for an auto-approved Write: it shows no diff until
the hook. That's out of scope here, since it changes AIR's output.
Two findings of the 2026-10-10 night perf run on a 5,000-session home
(findings 8 and 9).
## Rename push (finding 9)
The run measured "no push within 10 s" after `_session/rename`. The
cause was the harness, not the adapter. The bench renames the newest
session to `"Night renamed"`, and an earlier bench run on the same home
had already given it that title. The transcript shows that pair of title
records twice. A rename to the current title appends nothing, so there
was nothing to push. On a fresh home, the same bench gets the push. The
hypotheses checked:
- The metadata cache is not updated by a rename. The appended records
change the transcript's size, and the watcher re-reads it.
- The sidecar lies outside the non-recursive watch, but so does every
rename. Each one also appends to the transcript, which is watched.
Still, the push depended on `fs.watch` plus its 150 ms quiet period. Now
a rename, archive, unarchive or delete through this connection publishes
at once. `ListSubscriptions.sessionWritten` stats the session's
transcripts and reads its row, the way a transcript event would, without
waiting. The event that follows finds the row unchanged and sends
nothing. Renames by other processes still arrive through the watcher.
The 1 s per-session interval of a subscription still applies.
## Listing every page (finding 8)
Profiling `SessionIndex.list` showed where a page's time goes. The full
enumeration and stat of 5,000 transcripts takes about 20 ms per page.
Most of the remaining ~380 ms was the SDK `getSessionInfo`, called once
per row read: its lookup runs a `readdir` of the whole project directory
(5,000 entries) on every call.
- A metadata read no longer calls the SDK. Titles come from the head and
tail the index already reads, by the same rule (`sdkTitles`). The tail
is searched as the SDK searches it, cut first line included.
- The pages after a cursor reuse the list's enumeration. It is keyed by
the cursor and kept for at most 30 s, at most 8 lists. It is dropped as
soon as a directory of the scope adds or removes an entry, through the
`DirListings` identity check. Such a page stats only the transcripts it
reads (each with its session's other copies) right before reading them.
A transcript that changed since either keeps its order key (no new
prompt) or moves up before the cursor, so the page is as fresh as one
from a new enumeration. The first page always enumerates.
## Measurements
Real adapter (`dist/index.js`), driven as AIR drives it by
`acp-perf-harness/air-ui/night/acp-adapter-bench.mjs`. Fixture: 5,000
sessions from `gen-night-data.mjs`.
| | before | after |
|---|---|---|
| Listing every page (25 × 200) | 10.1–16.3 s | 0.92–1.16 s |
| First page (51) | 156–472 ms | 46–62 ms |
| Show more (50) | 117–283 ms | 13–23 ms |
| Push after archive | ~170 ms | 3–4 ms |
| Push after a rename | ~170 ms | 3–5 ms |
## Tests
- A rename, archive, unarchive and delete each push within 500 ms while
the watcher's quiet period is 60 s (fails without the fix). A rename by
another process is pushed through the watcher.
- Paging:
- one enumeration for all pages;
- between pages, a transcript emptied, one with a new prompt, and one
with only a new answer give the same pages as a new enumeration;
- an older copy of a session that got a prompt hides it on later pages;
- adding or removing a transcript enumerates again.
- A custom title whose record starts before the tail window matches the
SDK.
- Read-count assertions use the new `SessionIndex.transcriptsRead`
counter instead of SDK call counts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## Summary
Adds the opt-in AIR `sessionIndex` extension. With it, an AIR client can
show the latest sessions of a project quickly, with their state, and can
rename, archive and unarchive them over ACP. The full contract is in
`docs/air-extensions.md` ("Session index").
The extension follows ACP RFDs. The transport differs: it uses
`_meta.jetbrains.air` request options and row fields, `_session/*`
methods, and AIR capabilities that are not advertised under ACP v2. The
`archived` list filter also differs (see below).
- **Session list extensions**
(agentclientprotocol/agent-client-protocol#2323):
- `limit`, ordered by last user activity (`lastPromptAt ?? updatedAt`),
and `includeWorktrees`.
- Row fields `lastPromptAt`, `model` and `forkedFrom`.
- `state`: `running`, `idle`, `requires_action`, and `error`, which the
RFD lacks.
- `lastTurnEndedAt`.
- `cost`: the SDK's `total_cost_usd` for a session that runs on this
connection only. No cost is read from transcripts.
- Live updates come from an AIR list subscription (below).
- **Session archive** (agentclientprotocol/agent-client-protocol#2161):
- `archived` is on every row, `session_info_update` is sent on a change,
and an unknown session is `-32002`.
- The `archived` list filter has three values: `"unarchived"` (the
default), `"archived"` (archived sessions only) and `"all"` (both, in
one order). #2161 has a boolean here. Its `false` is `"unarchived"`, its
`true` is `"all"`, and `"archived"` has no counterpart.
- **Capabilities**: an AIR client that declares `sessionIndex` gets
these in `_meta.jetbrains.air.capabilities`. The client declares only
`sessionIndex`.
- `sessionIndex`.
- `sessionArchive`: `_session/archive` and `_session/unarchive`. The RFD
capability is `archive` (#2161).
- `sessionRename`: `_session/rename`. The RFD has capability `setTitle`
and method `session/set_title`
(agentclientprotocol/agent-client-protocol#1987).
- `sessionListSubscribe`: `_session/list/subscribe`,
`_session/list/unsubscribe` and `_session/list/changes`. No RFD
counterpart.
### Clients without `sessionIndex`
Clients that don't declare `sessionIndex`, which means non-AIR clients
and released AIR builds, keep the current behaviour. They get the same
SDK `listSessions` path, page size, cursor and initialize capabilities,
with no watchers and no notifications. There are two deliberate
deviations, both strictly safer:
1. **`session/delete` from an AIR client without `sessionIndex`.**
Released AIR builds use delete as "Done". Instead of deleting the
transcript (data loss), the adapter archives the session in AIR's own
Claude format (below). Archived sessions are hidden from that client's
list, so "Done" looks the same and nothing is lost. An unknown session
keeps the SDK's error. Other clients get the SDK delete, as before.
2. **`session/close`** of a session that is not loaded here returns `{}`
instead of throwing.
### Sessions open in another process
`session/load`, `session/resume` and `session/prompt` work on a session
that another live Claude Code process has open, for every client, as the
Claude CLI does. Rename, archive, unarchive and delete also go ahead,
writing records or deleting transcripts as the CLI's `/rename` and
delete do. The other process keeps running.
### Archive format and rule
The archive state uses AIR's native Claude archive format
(`ClaudeStoreThreadBackend.appendTitleRecords`, ADR 0140), so AIR's
native path and the adapter read and write the same state.
- **Archive** appends `{"type":"custom-title","customTitle":"[archived]
<title>",…}` and `{"type":"agent-name","agentName":"[archived]
<title>",…}` to every transcript copy. **Unarchive** appends the same
pair without the prefix. Titles are cut to the CLI's 200 characters.
- **Archived is decided by the agent name first**, as in AIR's native
Claude provider. A session's name is its last top-level `agentName`,
else the custom title the SDK reports. The session is archived exactly
when that name starts with `[archived] `. The listed title comes from
the same source, shown without the prefix.
- Title records are metadata: `updatedAt` and the list order do not
move.
### Rename, archive and delete
- **Rename** writes the same title pair. An archived session keeps its
prefix. A session running here is renamed through its CLI's
`rename_session` control request, serialised with auto-titling. Other
sessions get the records plus the `custom-title.json` sidecar the CLI
writes. No generated title replaces a client title, but a title stored
later by someone else (a `/rename`) is still adopted.
- **Archiving stops the running session.** `_session/archive` of a
session loaded on this connection does four things:
1. It interrupts the running turn.
2. It retitles the session through its CLI. The adapter also appends the
matching `agent-name` record to the CLI's transcript, because the CLI
writes that record only later.
3. It closes the session as `session/close` does.
4. It archives the transcripts once more if the closed CLI left them
unarchived.
The client then gets `session_info_update` with `archived: true`.
Unarchive does not reopen the session. A session that is not loaded here
is archived by writing the title records.
- **Delete** for a `sessionIndex` client deletes every copy of the
session and its `<sessionId>/` directories. A session running here is
closed first.
### Implementation (`src/session-index/`)
- **Index**:
- Reads the project dirs with `readdir` + `stat`, and keeps a 2000-entry
metadata LRU keyed by (path, mtime, size).
- Cache misses are read in parallel through `getSessionInfo`, plus a
growing tail read (64 KB → 4 MB) for the last prompt, last message, turn
end and agent name.
- SDK semantics are kept: sidecar titles, `continued-in`, long-path
hashes, darwin case, and first-prompt extraction (ported, and
fuzz-compared with the SDK).
- The `cwd` scope follows the SDK `listSessions`. Worktrees expand to
the same relative subdirectory, and a session copied to several project
directories is listed from its newest copy.
- The keyset cursor is bound to `cwd`, `archived` and
`includeWorktrees`. Session ids match exactly.
- **State**:
- For sessions of this process, the SDK state.
- For other sessions, the Claude Code live registry
(`~/.claude/sessions/<pid>.json`, with pid + procStart liveness and
Claude Desktop's rules) or the transcript tail. The registry is read
only for the row state.
- `error` means the session is not running and its last turn ended with
an error, not a cancel.
Documented in `docs/air-extensions.md`, with an extension → RFD mapping
table and the remaining differences.
### Measured on a real `~/.claude` (233 transcripts, read-only)
| Case | New path | Old SDK path |
|---|---|---|
| cwd, limit 50, cold process | ~80 ms | 78–156 ms (all rows) |
| cwd, limit 50, warm | 1.3–4 ms | — |
## Session list subscription
Replaces polling the first page. It is only for a client that declared
`sessionIndex`; the agent advertises `sessionListSubscribe` with it,
never under ACP v2.
- **Subscribe**: `_session/list/subscribe { cwd }` returns `{
subscriptionId }`. `cwd` must be absolute, else `-32602`; other params
are ignored.
- The scope is `cwd` and the same subdirectory in every linked worktree,
in any archive state, without sidechains.
- **Changes**: `_session/list/changes { subscriptionId, sessions,
removed }`.
- `sessions` holds the full `session/list` row of each session that
appeared, or whose title, `lastPromptAt`, `state`, `lastTurnEndedAt`,
`cost`, `model`, `forkedFrom` or `archived` changed since the last row
sent.
- The first change of a session after subscribe is sent in full. After
that, a change of `updatedAt` alone sends nothing.
- `removed` lists sessions that were deleted or moved out of scope.
- Archive and unarchive are row changes.
- A session is sent at most once a second, with one notification per
subscription per pass.
- **Unsubscribe**: `_session/list/unsubscribe { subscriptionId }`
returns `{}` and is idempotent.
- A subscription lives until unsubscribe or until the connection closes.
- A connection has at most 128 subscriptions. The 129th is `-32602` with
`data.reason: "too_many_subscriptions"`.
- Subscriptions of the same `cwd` share one watch and are notified
independently.
- **Delivery** is best effort, with no resync and no sequence numbers. A
rescan every 10 s (and 1 s after a project directory watch opens) sends
whatever differs.
- **Implementation** (`list-subscriptions.ts`, `session-info.ts`):
- A non-recursive `fs.watch` on the scope's project dirs, the registry
and the projects root.
- A transcript event re-reads only that transcript, and a registry event
reads only that `<pid>.json`. Sessions running here push their SDK state
at once.
- Subscribe reads no session row. The rescan reads only transcripts
whose stat changed or whose read failed.
### Measured: one subscription on the 5,200-session project (Docker
`node:22`, 2 CPU, 2 GB, shared host)
300 s of churn: 20 sessions appending 10/s each (updatedAt-only),
registry status flips every 200 ms, and a session created every 2 s and
deleted 10 s later.
| | Old `list_changed` watcher | Subscription |
|---|---|---|
| Agent CPU p50 / max | 19% / — | 12.9% / 49% |
| Event loop p99 (p50 / max of 1 s windows) | — | 16 / 31 ms; worst
single delay 87 ms |
| Change latency p50 / p95 | 950 ms (hint) | create 291 / 570 ms, delete
279 / 506 ms; registry state 902 / 990 ms* |
| Notifications / min | — | 90 (896 rows, 0 rows for updatedAt-only
appends) |
| RSS | flat ~160 MB | 159 → 198 MB (heap flat ~50 MB) |
\* The registry flips the same 5 sessions every 200 ms, so each is held
to the once-a-second limit. Latency is measured from the first unsent
change.
These numbers were measured with an earlier subscribe that read every
row up front. Subscribe now reads no rows, so that one-off cost no
longer occurs. The lazy subscription has not been re-measured yet.
## Load test: 10,000 sessions (Docker, 2 CPU, 2 GB)
Calibrated data: 10k transcripts, 15 GB, 36k subagent files, and one
5,200-session project. Sizes match a real profile (p50 311K / p90 2.67M
/ p99 20.5M).
| Scenario (limit 50) | Before | After |
|---|---|---|
| Big project, first page cold / warm p50 | out of memory | 225 / 19 ms
|
| No cwd, cold / warm p50 | 1,722 / 178 ms | 209 / 45 ms |
| Page through 4,637 rows | 457 s | 11.4 s cold, 2.9 s warm |
| Delete p50 / 100 concurrent | 23.8 ms / 4.4 s | 7.8 ms / 0.8 s |
| 300 s of churn (60k changes) | out of memory after 3 s | list p50 54
ms, 0 stalls >50 ms, RSS flat ~160 MB |
## Streaming: keep the event loop responsive under a backlog
Under a backlog of buffered SDK messages, the adapter could freeze its
event loop for seconds. Every `query.next()` on a non-empty SDK queue
resolves in a microtask, so `runConsumer` forwarded the whole backlog
without reaching a macrotask boundary. Incoming JSON-RPC, including
`session/cancel`, waited until the backlog drained.
1. **Consumer yields to the event loop.** A shared `EventLoopYielder`
(`src/utils.ts`) gives the event loop a turn via `setImmediate` once the
current iteration has run for 8 ms. Order, content and cancel semantics
are unchanged.
2. **No per-message copy in `nodeToWebWritable`** for byte streams.
Object-mode streams keep the copy.
3. **Diff memo.** The approval preview and the PostToolUse patch no
longer diff the same texts twice. Timed-out diffs are not remembered.
4. **Prefix guards** in `parseMcpCommand` and `sanitizeTitle`. Both give
identical output.
5. **Replay yields between history messages**, with the same budget.
With 10 sessions × 1 MB, the max event-loop delay went from 4205 ms to
1626 ms, and ping p95 from 2793 ms to 262 ms (interleaved A/B, Docker
`node:22`, 2 CPU). Single-stream throughput is unchanged. Remaining
stalls are inside the Claude SDK and GC.
## Test plan
- [x] Rebased on `origin/main`. `npm run check`, `npm run build` and
`npm run test:run` pass.
- [x] Regression tests for the unchanged non-`sessionIndex` paths.
- [x] Regression tests for every review finding.
- [x] Reviews: Codex (gpt-6-astra) and a Claude review subagent, run as
independent passes against `origin/main` until a round found nothing of
normal or higher severity.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The v2 draft lets the `session/new`, `session/resume`, and
`session/fork` responses carry `availableCommands`
([agent-client-protocol#2259](https://github.com/agentclientprotocol/agent-client-protocol/pull/2259)),
so a client can show a session's commands without waiting for an update.
The v2 surface now fills it.
## Before and after, for a v2 client
- **Before:** the setup response, then an `available_commands_update`
with the list, from a `setTimeout` after the response.
- **Now:** the list is in the response itself, for `session/new`,
`session/resume` with and without replay, and `session/fork`. The field
is left out when the list is empty, as the draft asks.
- **No duplicate:** on a v2 connection the agent no longer sends that
post-setup update (`afterSetupResponse`). Later changes still arrive as
`available_commands_update`, for example the terminal-bound filtering
after Claude Code's first init frame, or `commands_changed`.
- v1 and AIR are unchanged.
## Cost
None. The commands come from `Query.supportedCommands()`, which in the
SDK is `this.latestCommands ?? (await this.initialization).commands`:
the cached initialization result that setup already awaited. So the
response doesn't wait any longer.
## Code
- `ClaudeAcpAgent.availableCommands(sessionId)` returns the session's
command list, factored out of `sendAvailableCommandsUpdate`.
- The three post-setup `setTimeout` blocks become one
`afterSetupResponse`, which skips the command update on v2.
- `setupCommands` in `src/v2/agent.ts` adds the list in v2 form. A
failure to read the commands is logged and leaves them out, since
they're optional and the session already exists.
## Title
`refactor:`, like the earlier v2 surface additions (#1253, #1255,
#1256), so no release note announces v2 yet. Retitle it if you'd rather.
## Checks
- `npm run check`, `npm run build` and all 2494 tests pass.
- The 40 v2 golden files each move the list from the post-setup
notification into the setup response, and nothing else changes; I
checked every diff with a script. No v1 or AIR golden file changes.
- New v2 tests cover each setup response (new, resume, resume with
replay, fork) and check that no post-setup update follows.
- I removed each part in turn, and each removal fails tests: the agent
still sending the update on v2, and the commands missing from new, from
resume, or from fork.
- Not covered: the empty-list case, because the adapter always adds its
`/mcp` command.
v2 has a `cancelled` tool call status ("the tool call was cancelled
before it completed"), which v1 doesn't. Until now a v2 client got
`failed` for a tool call the user interrupted, the same as a real
failure.
## What changes
Claude Code stamps a tool result that carries no tool output with
`tool_result_meta.non_execution_kind`. For a **v2 client**:
| Kind | Meaning (set in the bundled CLI) | Status |
| --- | --- | --- |
| `interrupted` | the turn was interrupted while the tool ran |
`cancelled` |
| `cancelled` | the call's own run was stopped (aborted, or moved to the
background) | `cancelled` |
| `user-rejected`, `permission-rule`, `automode-*` | the tool was
refused | `failed`, as before |
v1 and AIR clients are unchanged: `failed`, with the kind in
`_meta.claudeCode.nonExecutionKind`. v2 clients still get that `_meta`
too.
## How
- **The renderer decides the status**
(`AcpToolCallRenderer.resultStatus`), from `ClientCapabilities.v2`.
That's the same pattern the agent uses for v2 diffs: the value travels
as a v1 status (`V2_CANCELLED`, with a cast, since v1's `ToolCallStatus`
is closed), and the v2 surface passes it through. The v2 translation
still doesn't read `_meta`. The rule covers the regular result and the
two fallback reports in `toAcpNotifications`: an untracked tool call,
and a TodoWrite/Task tool call that only a permission request surfaced.
- **The agent's own "finished" checks** now use `isFinalToolCallStatus`,
which counts `cancelled`. Two places compared against
`completed`/`failed`: one removes a finished call from the turn's
foreground work, the other drops its eager-session and dispatched-call
entries. Without this, a cancelled call would have leaked those entries.
## Not covered
Replay doesn't have `tool_result_meta`, so a replayed interrupted call
stays `failed`, just as v1 replay has no `nonExecutionKind`.
## Checks
- `npm run check`, `npm run build` and all 2493 tests pass. No golden
file changes: no recorded scenario has an interrupted tool.
- New tests:
- **renderer:** each kind, for v2, v1 and AIR clients, plus the
permission-surfaced fallback;
- **agent:** a cancelled call is released from the dispatched-call
bookkeeping;
- **v2 surface:** a v2 client receives `cancelled`.
- I removed each part of the change in turn, and each removal fails a
test: never cancelled, cancelled for v1 too, refusals cancelled too, the
bookkeeping ignoring `cancelled`, and the fallback branches keeping
`failed`.
Follow-up to #1287 (SDK 1.8.0). A v2 turn that fails after its prompt
was taken in now ends with the standard `error` stop reason from
[agent-client-protocol#2281](https://github.com/agentclientprotocol/agent-client-protocol/pull/2281),
in place of the stand-ins we used while the SDK didn't have it.
## Before and after
Before, on the v2 surface only:
1. an error `notice`, titled with the failure;
2. `idle` with the custom stop reason `_error`, and the JSON-RPC error
in `_meta.claudeCode.error`.
Now, one update:
```json
{ "sessionUpdate": "state_update", "state": "idle", "stopReason": "error",
"error": { "code": -32603, "message": "Internal error: API Error: 529 Overloaded" } }
```
- `error` is the JSON-RPC error that v1 rejects the prompt with: same
code, message and data. A client can show `message` and handle `code` as
for an error response, so `-32000` (auth required) can start its sign-in
again, which `_error` couldn't.
- The notice is gone. The v2 draft's
[Failures](https://agentclientprotocol.com/protocol/v2/draft/prompt-lifecycle)
section reports a failure that ends the work through the stop reason,
and keeps notices for failures the agent recovers from.
- v1 and AIR are unchanged.
## Also
- `TurnEvents.failed` loses its `title` parameter. Only the v2 notice
read it, and v1's prompt already ignored it. Its pass-through in
`Turn.reject` and `failActive` goes too. AIR's session failures still
get the title, through their own path.
- The v2 scenario harness treats an `error` stop reason as a failed
turn, and reports its message.
## One visible difference
The old notice showed the failure title, for example `API Error: 529
Overloaded`. The error's message is v1's, `Internal error: API Error:
529 Overloaded`, which v1 clients already show. If we'd rather v2
clients see the shorter text, the alternative is to use the title as the
error's `message` and keep v1's code and data. That would make v2's
message differ from v1's.
## Checks
- `npm run check`, `npm run build` and all 2489 tests pass. No golden
file changes: no recorded scenario fails a turn after insertion.
- The two v2 failure tests pin the new update and the auth code. Putting
`_error` back fails both.
Bumps `@agentclientprotocol/sdk` from 1.7.0 to 1.8.0. The release is one
change: the protocol schema moves to v1.25.0 and v2.0.0-alpha.8.
## What changed in the SDK
Comparing the generated types of 1.7.0 and 1.8.0, the only type change
in v1 and v2 is the `error` stop reason from
[agent-client-protocol#2281](https://github.com/agentclientprotocol/agent-client-protocol/pull/2281):
`IdleStateUpdate` gains `ErrorStopReason & { stopReason: "error" }`, and
`ErrorStopReason` (an optional JSON-RPC `error`) is new. The v2 index no
longer exports `StopReason`; the adapter doesn't import it.
## The one code change
`idle()` in `src/v2/prompt.ts` built the v2 `state_update` for an ended
turn by spreading the turn's stop reason into a `SessionUpdate`. With
the larger `IdleStateUpdate` union, TypeScript can no longer match a
union of stop reasons against it (it falls through to the custom and
unknown members). The state is now typed as the members for the agent's
own stop reasons (`Extract<IdleStateUpdate, { stopReason: StopReason
}>`). A stop reason that v2 doesn't know is still a compile error, which
I checked with a throwaway probe. The output is unchanged.
## Not in this PR
The adapter doesn't use the new `error` stop reason yet. Two stand-ins
stay until a follow-up switches them, which changes behavior and golden
files:
- a failed v2 turn's `_error` stop reason with `_meta.claudeCode.error`,
and its error `notice` (`src/v2/prompt.ts`);
- a failed subagent's `_meta.claudeCode.error` in the RFD form (#1257,
not merged yet).
## Checks
- `npm run check`, `npm run build`, and all 2489 tests pass.
- The scenario suites validate every message against the schemas that
1.8.0 ships (`schema/schema.json`, `schema/v2/schema.unstable.json`),
loaded from the installed SDK. No golden file changes.
## What
ACP `logout` cleared `providerConfig` and the gateway credentials but
left live queries running. Provider and gateway routing is baked into
each query's env, so open sessions kept using the old route after
logout.
`logout` now recreates every live session through the same path as
`providers/disable` (`enqueueProviderUpdate`), but only when a provider
or gateway override was active. It waits for active turns, then resumes
each session under its id with native routing. This also runs when
`claude auth logout` fails, because the override is already cleared by
then.
A plain CLI-login logout does not touch live sessions: the running CLI
picks up the logout by itself.
## Behavior change
With an override active, `logout` now waits for running turns before it
returns, like `providers/disable`. Under `--hide-claude-auth`, a session
that cannot be recreated signed out is dropped, as with
`providers/disable`.
Bumps the minor group with 12 updates in the / directory:
| Package | From | To |
| --- | --- | --- |
|
[@anthropic-ai/claude-agent-sdk](https://github.com/anthropics/claude-agent-sdk-typescript)
| `0.3.287` | `0.3.289` |
| [eslint](https://github.com/eslint/eslint) | `10.11.0` | `10.12.0` |
| @anthropic-ai/claude-agent-sdk-darwin-arm64 | `0.3.287` | `0.3.289` |
| @anthropic-ai/claude-agent-sdk-darwin-x64 | `0.3.287` | `0.3.289` |
| @anthropic-ai/claude-agent-sdk-linux-arm64 | `0.3.287` | `0.3.289` |
| @anthropic-ai/claude-agent-sdk-linux-arm64-musl | `0.3.287` |
`0.3.289` |
| @anthropic-ai/claude-agent-sdk-linux-x64 | `0.3.287` | `0.3.289` |
| @anthropic-ai/claude-agent-sdk-linux-x64-musl | `0.3.287` | `0.3.289`
|
| @anthropic-ai/claude-agent-sdk-win32-arm64 | `0.3.287` | `0.3.289` |
| @anthropic-ai/claude-agent-sdk-win32-x64 | `0.3.287` | `0.3.289` |
|
[@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk)
| `1.31.0` | `1.32.1` |
| [hono](https://github.com/honojs/hono) | `4.13.12` | `4.13.13` |
Updates `@anthropic-ai/claude-agent-sdk` from 0.3.287 to 0.3.289
<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.289</h2>
<h2>What's changed</h2>
<ul>
<li>Updated to parity with Claude Code v2.1.289</li>
</ul>
<h2>Update</h2>
<pre lang="sh"><code>npm install @anthropic-ai/claude-agent-sdk@0.3.289
# or
yarn add @anthropic-ai/claude-agent-sdk@0.3.289
# or
pnpm add @anthropic-ai/claude-agent-sdk@0.3.289
# or
bun add @anthropic-ai/claude-agent-sdk@0.3.289
</code></pre>
<h2>v0.3.288</h2>
<h2>What's changed</h2>
<ul>
<li>Updated to parity with Claude Code v2.1.288</li>
</ul>
<h2>Update</h2>
<pre lang="sh"><code>npm install @anthropic-ai/claude-agent-sdk@0.3.288
# or
yarn add @anthropic-ai/claude-agent-sdk@0.3.288
# or
pnpm add @anthropic-ai/claude-agent-sdk@0.3.288
# or
bun add @anthropic-ai/claude-agent-sdk@0.3.288
</code></pre>
</blockquote>
</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.289</h2>
<ul>
<li>Updated to parity with Claude Code v2.1.289</li>
</ul>
<h2>0.3.288</h2>
<ul>
<li>Updated to parity with Claude Code v2.1.288</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="16cf0a7831"><code>16cf0a7</code></a>
chore: Update CHANGELOG.md</li>
<li><a
href="36836f000b"><code>36836f0</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.287...v0.3.289">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 <code>@anthropic-ai/claude-agent-sdk</code> since your
current version.</p>
</details>
<br />
Updates `eslint` from 10.11.0 to 10.12.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/eslint/eslint/releases">eslint's
releases</a>.</em></p>
<blockquote>
<h2>v10.12.0</h2>
<h2>Features</h2>
<ul>
<li><a
href="4618052eed"><code>4618052</code></a>
feat: handle astral letters in <code>new-cap</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/21357">#21357</a>)
(sary)</li>
<li><a
href="4ec5168d6d"><code>4ec5168</code></a>
feat: allow <code>SourceCode#getText()</code> to accept tokens and
comments (<a
href="https://redirect.github.com/eslint/eslint/issues/21340">#21340</a>)
(electrohyun)</li>
</ul>
<h2>Bug Fixes</h2>
<ul>
<li><a
href="bc51eee6e3"><code>bc51eee</code></a>
fix: <code>prefer-arrow-callback</code> false positive in conditional
test (<a
href="https://redirect.github.com/eslint/eslint/issues/21373">#21373</a>)
(Daniel Pinto)</li>
<li><a
href="bbff86c415"><code>bbff86c</code></a>
fix: skip lines with multiple comments in
<code>max-lines-per-function</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/21332">#21332</a>)
(xbinaryx)</li>
<li><a
href="efc4d6bdc4"><code>efc4d6b</code></a>
fix: astral letters in <code>consistent-return</code>,
<code>no-eval</code>, <code>no-invalid-this</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/21360">#21360</a>)
(lumir)</li>
<li><a
href="93de066d41"><code>93de066</code></a>
fix: prefer-exponentiation-operator autofix for async function base (<a
href="https://redirect.github.com/eslint/eslint/issues/21322">#21322</a>)
(Vladimir Babin)</li>
<li><a
href="02e34ffba6"><code>02e34ff</code></a>
fix: add missing space after <code>else</code> in <code>curly</code>
autofix (<a
href="https://redirect.github.com/eslint/eslint/issues/21355">#21355</a>)
(Pixel)</li>
<li><a
href="b14b8bc213"><code>b14b8bc</code></a>
fix: correct <code>id-length</code> message for long private names (<a
href="https://redirect.github.com/eslint/eslint/issues/21348">#21348</a>)
(Pixel)</li>
<li><a
href="69aac01d89"><code>69aac01</code></a>
fix: support <code>TSFunctionType</code> in
<code>getFunctionHeadLoc</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/21335">#21335</a>)
(xbinaryx)</li>
<li><a
href="686630eb34"><code>686630e</code></a>
fix: <code>no-loss-of-precision</code> false positive with
<code>0.e5</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/21337">#21337</a>)
(sethamus)</li>
</ul>
<h2>Documentation</h2>
<ul>
<li><a
href="67eb586e2e"><code>67eb586</code></a>
docs: Update README (GitHub Actions Bot)</li>
<li><a
href="5370d7ee35"><code>5370d7e</code></a>
docs: clarify <code>one-var</code> <code>separateRequires</code> matches
any <code>require()</code> call (<a
href="https://redirect.github.com/eslint/eslint/issues/21192">#21192</a>)
(sethamus)</li>
<li><a
href="8816c1ddfc"><code>8816c1d</code></a>
docs: Update README (GitHub Actions Bot)</li>
<li><a
href="3d2e7cedb7"><code>3d2e7ce</code></a>
docs: fix typo in no-unused-expressions documentation (<a
href="https://redirect.github.com/eslint/eslint/issues/21346">#21346</a>)
(bytedoe)</li>
</ul>
<h2>Chores</h2>
<ul>
<li><a
href="152067f0f8"><code>152067f</code></a>
chore: update ecosystem plugins (<a
href="https://redirect.github.com/eslint/eslint/issues/21362">#21362</a>)
(ESLint Bot)</li>
<li><a
href="b56d58e0b1"><code>b56d58e</code></a>
chore: update github/codeql-action action to v4.38.2 (<a
href="https://redirect.github.com/eslint/eslint/issues/21376">#21376</a>)
(renovate[bot])</li>
<li><a
href="bfaea12ddc"><code>bfaea12</code></a>
perf: cache normalized config globals per languageOptions (<a
href="https://redirect.github.com/eslint/eslint/issues/21364">#21364</a>)
(James Ross)</li>
<li><a
href="322209ef16"><code>322209e</code></a>
ci: avoid Nx cache in ecosystem tests and disable failing test (<a
href="https://redirect.github.com/eslint/eslint/issues/21369">#21369</a>)
(Francesco Trotta)</li>
<li><a
href="d166567901"><code>d166567</code></a>
chore: update dependency prettier to v3.9.9 (<a
href="https://redirect.github.com/eslint/eslint/issues/21371">#21371</a>)
(renovate[bot])</li>
<li><a
href="29585ceba7"><code>29585ce</code></a>
chore: update dependency eslint-plugin-expect-type to ^0.7.0 (<a
href="https://redirect.github.com/eslint/eslint/issues/21359">#21359</a>)
(renovate[bot])</li>
<li><a
href="39d79ba965"><code>39d79ba</code></a>
chore: update github/codeql-action action to v4.38.1 (<a
href="https://redirect.github.com/eslint/eslint/issues/21354">#21354</a>)
(renovate[bot])</li>
<li><a
href="182a6e993a"><code>182a6e9</code></a>
chore: update dependency prettier to v3.9.8 (<a
href="https://redirect.github.com/eslint/eslint/issues/21352">#21352</a>)
(renovate[bot])</li>
<li><a
href="f995127230"><code>f995127</code></a>
chore: remove CLAUDE.md in favor of AGENTS.md (<a
href="https://redirect.github.com/eslint/eslint/issues/21339">#21339</a>)
(Jarren)</li>
<li><a
href="b95fb6cb33"><code>b95fb6c</code></a>
chore: update dependency prettier to v3.9.7 (<a
href="https://redirect.github.com/eslint/eslint/issues/21347">#21347</a>)
(renovate[bot])</li>
<li><a
href="3782dd458f"><code>3782dd4</code></a>
chore: update ecosystem plugins (<a
href="https://redirect.github.com/eslint/eslint/issues/21342">#21342</a>)
(ESLint Bot)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="a438ec3951"><code>a438ec3</code></a>
10.12.0</li>
<li><a
href="32a73f1441"><code>32a73f1</code></a>
Build: changelog update for 10.12.0</li>
<li><a
href="bc51eee6e3"><code>bc51eee</code></a>
fix: <code>prefer-arrow-callback</code> false positive in conditional
test (<a
href="https://redirect.github.com/eslint/eslint/issues/21373">#21373</a>)</li>
<li><a
href="bbff86c415"><code>bbff86c</code></a>
fix: skip lines with multiple comments in
<code>max-lines-per-function</code> (<a
href="https://redirect.github.com/eslint/eslint/issues/21332">#21332</a>)</li>
<li><a
href="152067f0f8"><code>152067f</code></a>
chore: update ecosystem plugins (<a
href="https://redirect.github.com/eslint/eslint/issues/21362">#21362</a>)</li>
<li><a
href="b56d58e0b1"><code>b56d58e</code></a>
chore: update github/codeql-action action to v4.38.2 (<a
href="https://redirect.github.com/eslint/eslint/issues/21376">#21376</a>)</li>
<li><a
href="67eb586e2e"><code>67eb586</code></a>
docs: Update README</li>
<li><a
href="bfaea12ddc"><code>bfaea12</code></a>
perf: cache normalized config globals per languageOptions (<a
href="https://redirect.github.com/eslint/eslint/issues/21364">#21364</a>)</li>
<li><a
href="322209ef16"><code>322209e</code></a>
ci: avoid Nx cache in ecosystem tests and disable failing test (<a
href="https://redirect.github.com/eslint/eslint/issues/21369">#21369</a>)</li>
<li><a
href="d166567901"><code>d166567</code></a>
chore: update dependency prettier to v3.9.9 (<a
href="https://redirect.github.com/eslint/eslint/issues/21371">#21371</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/eslint/eslint/compare/v10.11.0...v10.12.0">compare
view</a></li>
</ul>
</details>
<br />
Updates `@anthropic-ai/claude-agent-sdk-darwin-arm64` from 0.3.287 to
0.3.289
<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 <code>@anthropic-ai/claude-agent-sdk-darwin-arm64</code>
since your current version.</p>
</details>
<br />
Updates `@anthropic-ai/claude-agent-sdk-darwin-x64` from 0.3.287 to
0.3.289
<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 <code>@anthropic-ai/claude-agent-sdk-darwin-x64</code>
since your current version.</p>
</details>
<br />
Updates `@anthropic-ai/claude-agent-sdk-linux-arm64` from 0.3.287 to
0.3.289
<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 <code>@anthropic-ai/claude-agent-sdk-linux-arm64</code>
since your current version.</p>
</details>
<br />
Updates `@anthropic-ai/claude-agent-sdk-linux-arm64-musl` from 0.3.287
to 0.3.289
<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
<code>@anthropic-ai/claude-agent-sdk-linux-arm64-musl</code> since your
current version.</p>
</details>
<br />
Updates `@anthropic-ai/claude-agent-sdk-linux-x64` from 0.3.287 to
0.3.289
<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 <code>@anthropic-ai/claude-agent-sdk-linux-x64</code>
since your current version.</p>
</details>
<br />
Updates `@anthropic-ai/claude-agent-sdk-linux-x64-musl` from 0.3.287 to
0.3.289
<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 <code>@anthropic-ai/claude-agent-sdk-linux-x64-musl</code>
since your current version.</p>
</details>
<br />
Updates `@anthropic-ai/claude-agent-sdk-win32-arm64` from 0.3.287 to
0.3.289
<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 <code>@anthropic-ai/claude-agent-sdk-win32-arm64</code>
since your current version.</p>
</details>
<br />
Updates `@anthropic-ai/claude-agent-sdk-win32-x64` from 0.3.287 to
0.3.289
<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 <code>@anthropic-ai/claude-agent-sdk-win32-x64</code>
since your current version.</p>
</details>
<br />
Updates `@modelcontextprotocol/sdk` from 1.31.0 to 1.32.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/modelcontextprotocol/typescript-sdk/releases">@modelcontextprotocol/sdk's
releases</a>.</em></p>
<blockquote>
<h2>1.32.1</h2>
<h2>What's Changed</h2>
<ul>
<li>[v1.x] docs: state the principles in CLAUDE.md by <a
href="https://github.com/claude"><code>@claude</code></a>[bot] in <a
href="https://redirect.github.com/modelcontextprotocol/typescript-sdk/pull/2939">modelcontextprotocol/typescript-sdk#2939</a></li>
<li>[v1.x] docs: point the README at v2 and say what v1.x supports by <a
href="https://github.com/claude"><code>@claude</code></a>[bot] in <a
href="https://redirect.github.com/modelcontextprotocol/typescript-sdk/pull/2942">modelcontextprotocol/typescript-sdk#2942</a></li>
<li>chore: bump version to 1.32.1 by <a
href="https://github.com/claude"><code>@claude</code></a>[bot] in <a
href="https://redirect.github.com/modelcontextprotocol/typescript-sdk/pull/2954">modelcontextprotocol/typescript-sdk#2954</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/modelcontextprotocol/typescript-sdk/compare/1.32.0...1.32.1">https://github.com/modelcontextprotocol/typescript-sdk/compare/1.32.0...1.32.1</a></p>
<h2>1.32.0</h2>
<h2>Upgrade notes</h2>
<ul>
<li><strong>Redirects:</strong> the HTTP client transports now follow a
redirect only when it stays on the same origin (same scheme, host and
port; http to https on the same host is allowed). A deployment whose
endpoint redirects to another host or port either configures the final
URL or sets <code>redirectPolicy: 'follow'</code> on
<code>StreamableHTTPClientTransport</code> or
<code>SSEClientTransport</code>. In browsers, a redirected request fails
unless that option is set.</li>
<li><strong>New options, both off unless you set them:</strong>
<code>maxToolInputElements</code> on <code>McpServer</code> limits the
number of array elements and object members in a tool call's arguments.
<code>expectedResource</code> on <code>requireBearerAuth</code> accepts
only tokens issued for this server (the token's audience).</li>
</ul>
<h2>What's Changed</h2>
<ul>
<li>[v1.x] fix(client): follow redirects only within the endpoint's
origin by <a
href="https://github.com/claude"><code>@claude</code></a>[bot] in <a
href="https://redirect.github.com/modelcontextprotocol/typescript-sdk/pull/2902">modelcontextprotocol/typescript-sdk#2902</a></li>
<li>docs: point SECURITY.md at GitHub Security Advisories (v1.x) by <a
href="https://github.com/claude"><code>@claude</code></a>[bot] in <a
href="https://redirect.github.com/modelcontextprotocol/typescript-sdk/pull/2910">modelcontextprotocol/typescript-sdk#2910</a></li>
<li>[v1.x] examples: close idle sessions and cap the session map by <a
href="https://github.com/maxisbey"><code>@maxisbey</code></a> in <a
href="https://redirect.github.com/modelcontextprotocol/typescript-sdk/pull/2914">modelcontextprotocol/typescript-sdk#2914</a></li>
<li>[v1.x] fix(tasks): keep tasks of the in-memory task store within the
session that created them by <a
href="https://github.com/claude"><code>@claude</code></a>[bot] in <a
href="https://redirect.github.com/modelcontextprotocol/typescript-sdk/pull/2925">modelcontextprotocol/typescript-sdk#2925</a></li>
<li>[v1.x] feat(server): add maxToolInputElements option to limit the
number of elements in tool-call arguments by <a
href="https://github.com/claude"><code>@claude</code></a>[bot] in <a
href="https://redirect.github.com/modelcontextprotocol/typescript-sdk/pull/2927">modelcontextprotocol/typescript-sdk#2927</a></li>
<li>[v1.x] fix(server): accept tools/call and prompts/get requests that
omit arguments by <a
href="https://github.com/raashish1601"><code>@raashish1601</code></a>
in <a
href="https://redirect.github.com/modelcontextprotocol/typescript-sdk/pull/2045">modelcontextprotocol/typescript-sdk#2045</a></li>
<li>[v1.x] feat(auth): add expectedResource to requireBearerAuth by <a
href="https://github.com/claude"><code>@claude</code></a>[bot] in <a
href="https://redirect.github.com/modelcontextprotocol/typescript-sdk/pull/2930">modelcontextprotocol/typescript-sdk#2930</a></li>
<li>[v1.x] test(e2e): cover tools/call and prompts/get without arguments
by <a href="https://github.com/claude"><code>@claude</code></a>[bot] in
<a
href="https://redirect.github.com/modelcontextprotocol/typescript-sdk/pull/2931">modelcontextprotocol/typescript-sdk#2931</a></li>
<li>chore: bump version to 1.32.0 by <a
href="https://github.com/claude"><code>@claude</code></a>[bot] in <a
href="https://redirect.github.com/modelcontextprotocol/typescript-sdk/pull/2935">modelcontextprotocol/typescript-sdk#2935</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/raashish1601"><code>@raashish1601</code></a>
made their first contribution in <a
href="https://redirect.github.com/modelcontextprotocol/typescript-sdk/pull/2045">modelcontextprotocol/typescript-sdk#2045</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/modelcontextprotocol/typescript-sdk/compare/1.31.0...1.32.0">https://github.com/modelcontextprotocol/typescript-sdk/compare/1.31.0...1.32.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="ff07b00119"><code>ff07b00</code></a>
chore: bump version to 1.32.1 (<a
href="https://redirect.github.com/modelcontextprotocol/typescript-sdk/issues/2954">#2954</a>)</li>
<li><a
href="8a74d504a9"><code>8a74d50</code></a>
[v1.x] docs: point the README at v2 and say what v1.x supports (<a
href="https://redirect.github.com/modelcontextprotocol/typescript-sdk/issues/2942">#2942</a>)</li>
<li><a
href="a8cf50360f"><code>a8cf503</code></a>
[v1.x] docs: state the principles in CLAUDE.md (<a
href="https://redirect.github.com/modelcontextprotocol/typescript-sdk/issues/2939">#2939</a>)</li>
<li><a
href="32549b00a9"><code>32549b0</code></a>
chore: bump version to 1.32.0 (<a
href="https://redirect.github.com/modelcontextprotocol/typescript-sdk/issues/2935">#2935</a>)</li>
<li><a
href="588d51dbe3"><code>588d51d</code></a>
[v1.x] test(e2e): cover tools/call and prompts/get without arguments (<a
href="https://redirect.github.com/modelcontextprotocol/typescript-sdk/issues/2931">#2931</a>)</li>
<li><a
href="5a0724d14c"><code>5a0724d</code></a>
[v1.x] feat(auth): add expectedResource to requireBearerAuth (<a
href="https://redirect.github.com/modelcontextprotocol/typescript-sdk/issues/2930">#2930</a>)</li>
<li><a
href="0ca0b62d93"><code>0ca0b62</code></a>
[v1.x] fix(server): accept tools/call and prompts/get requests that omit
argu...</li>
<li><a
href="fd26801b3b"><code>fd26801</code></a>
[v1.x] feat(server): add maxToolInputElements option to limit the number
of e...</li>
<li><a
href="727075b5bb"><code>727075b</code></a>
[v1.x] fix(tasks): keep tasks of the in-memory task store within the
session ...</li>
<li><a
href="0ff63772da"><code>0ff6377</code></a>
[v1.x] examples: close idle sessions and cap the session map (<a
href="https://redirect.github.com/modelcontextprotocol/typescript-sdk/issues/2914">#2914</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/modelcontextprotocol/typescript-sdk/compare/1.31.0...1.32.1">compare
view</a></li>
</ul>
</details>
<br />
Updates `hono` from 4.13.12 to 4.13.13
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/honojs/hono/releases">hono's
releases</a>.</em></p>
<blockquote>
<h2>v4.13.13</h2>
<h2>Mount Middleware</h2>
<p><code>app.mount()</code> is now available as the Mount Middleware,
<code>hono/mount</code>. It is just a handler, so you register it with
<code>app.all()</code>:</p>
<pre lang="ts"><code>import { Router as IttyRouter } from 'itty-router'
import { Hono } from 'hono'
import { mount } from 'hono/mount'
<p>const ittyRouter = IttyRouter()<br />
ittyRouter.get('/hello', () => new Response('Hello from
itty-router'))</p>
<p>const app = new Hono()<br />
app.all('/itty-router/*', mount(ittyRouter.handle))<br />
</code></pre></p>
<p><code>app.mount()</code> still works in v4 but is deprecated and will
be removed in v5. Migrating is a one-line change:</p>
<pre lang="diff"><code>- app.mount('/itty-router', ittyRouter.handle)
+ app.all('/itty-router/*', mount(ittyRouter.handle))
</code></pre>
<h2>What's Changed</h2>
<ul>
<li>test(client): simulate network error for undefined route in
parseResponse test in <a
href="https://redirect.github.com/honojs/hono/pull/5439">honojs/hono#5439</a></li>
<li>docs(request): fix jsdoc comments for some getters in <a
href="https://redirect.github.com/honojs/hono/pull/5445">honojs/hono#5445</a></li>
<li>fix(jsx): allow JSXNode function component results in <a
href="https://redirect.github.com/honojs/hono/pull/5476">honojs/hono#5476</a></li>
<li>feat(mount): introduce Mount Middleware and deprecate
<code>app.mount</code> in <a
href="https://redirect.github.com/honojs/hono/pull/5221">honojs/hono#5221</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/honojs/hono/compare/v4.13.12...v4.13.13">https://github.com/honojs/hono/compare/v4.13.12...v4.13.13</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="08a023cbfd"><code>08a023c</code></a>
4.13.13</li>
<li><a
href="ae595de8ae"><code>ae595de</code></a>
feat(mount): introduce Mount Middleware and deprecate
<code>app.mount</code> (<a
href="https://redirect.github.com/honojs/hono/issues/5221">#5221</a>)</li>
<li><a
href="f23b146afc"><code>f23b146</code></a>
fix(jsx): allow JSXNode function component results (<a
href="https://redirect.github.com/honojs/hono/issues/5476">#5476</a>)</li>
<li><a
href="6d73a74f97"><code>6d73a74</code></a>
docs(request): fix jsdoc comments for some getters (<a
href="https://redirect.github.com/honojs/hono/issues/5445">#5445</a>)</li>
<li><a
href="deff529656"><code>deff529</code></a>
test(client): simulate network error for undefined route in
parseResponse tes...</li>
<li>See full diff in <a
href="https://github.com/honojs/hono/compare/v4.13.12...v4.13.13">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>
## Motivation
A prompt sent while Claude Code runs a turn it started on its own, for
example after a background task finished, is folded into that turn. The
turn's result keeps `origin: task-notification` but lists the prompt in
`user_message_uuids`. The adapter routed results by origin only, so
`session/prompt` was never answered and the client stayed in a running
state. A JetBrains AIR session stayed "running" for hours this way.
## Change
A task-notification result that names a pending prompt now answers it. A
turn held for its subagents still waits for them. Results without the
uuids behave as before.
## Tests
Unit tests in `acp-agent.test.ts` and a replay of the session in
`folded-prompt-scenario.test.ts`. Both hang without the fix.
Bumps the minor group with 2 updates:
[@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)
and [ip-address](https://github.com/beaugunderson/ip-address).
Updates `@types/node` from 26.6.3 to 26.6.4
<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 `ip-address` from 10.7.2 to 10.7.3
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/beaugunderson/ip-address/releases">ip-address's
releases</a>.</em></p>
<blockquote>
<h2>v10.7.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Reject an in-addr.arpa name longer than 32 characters before
splitting it by <a
href="https://github.com/beaugunderson"><code>@beaugunderson</code></a>
in <a
href="https://redirect.github.com/beaugunderson/ip-address/pull/228">beaugunderson/ip-address#228</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/beaugunderson/ip-address/compare/v10.7.2...v10.7.3">https://github.com/beaugunderson/ip-address/compare/v10.7.2...v10.7.3</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="c7f838eeca"><code>c7f838e</code></a>
10.7.3</li>
<li><a
href="09b8072f5e"><code>09b8072</code></a>
Reject an in-addr.arpa name longer than 32 characters before splitting
it (<a
href="https://redirect.github.com/beaugunderson/ip-address/issues/228">#228</a>)</li>
<li>See full diff in <a
href="https://github.com/beaugunderson/ip-address/compare/v10.7.2...v10.7.3">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>
It affects AIR clients with the `asyncTasks` capability.
## Problem
- A subagent can start a background Bash or Monitor and then end its
turn. The task ends after the subagent finished. The route of the task
dropped every update of a finished child generation, so the client never
got the terminal `async_task_state_update`. The task stayed running in
the client forever. One AIR log held 440 such dropped updates.
- The `Monitor` tool streams the stdout of a background command to the
model only. The SDK reports its task as `local_bash`, so the client
showed it as a background shell. In one real session, 18 of 18 Monitor
tasks had no output file, no progress and no result. The Stop button
only broke the agent's wait.
- The adapter read a task id from the text of a Bash tool result. A
foreground `sed`, `strings` or `python3` that printed adapter source,
test fixtures or CLI strings became a background task, for example with
the id `";\n const start = text` or `${e}`. The client got a spawn and a
`backgrounded` marker on a foreground call, and the task never ended.
- Claude Code 2.1.287 reports a background command in four texts, and
only one gave an output path. A subagent tool result almost never has
`tool_use_result` (0 of 48 in one session). So a subagent command that
hit its timeout got no output path (9 of 10).
## Fix
- `NativeSubagentRuntime.routeOfToolCall` still sends
`async_task_spawned`, `async_task_progress` and
`async_task_state_update` to the child generation that owned the tool
call after that child finished. It never sends them to a later
generation. Every other late update is dropped as before. The spawn is
in the list because a held task can get its tool call id after the
subagent finished.
- `AsyncTaskRuntime` takes a `toolNameOf` lookup. A task of a `Monitor`
tool call is ignored, like a subagent task: no spawn, no progress, no
state, and no stop. A held task that gets its tool call id later goes
through the same check. A `local_monitor` task is ignored too. The
`Monitor` tool call stays in the transcript.
- A task comes only from structured data:
`tool_use_result.backgroundTaskId` or the SDK task events. The text id
parser is gone.
- The output path of a known task can come from the result of its own
tool call. The path ends with `tasks/<id>.output` and starts after the
nearest `": "` on the same line, so a path with spaces works, for
example under `C:\Users\John Doe\AppData\Local\Temp`. It must be
absolute (POSIX, a Windows drive or UNC), and it never holds the end of
another path. The parse is linear in the text length. All four texts
give the path in both event orders: the tool result before or after the
SDK names the task. The memory is bounded. A structured path still wins,
and the terminal `output_file` reaches the client also after a level
close.
- The `backgrounded` marker comes only from structured data: the
structured task, a known background task of the tool call, or a
`run_in_background` input. The "both event orders" above holds for the
path, not for this marker. The marker rides the tool result update, so a
command that the SDK names only after its tool result gets the marker
only from a `run_in_background` input. A command that the user moved to
the background by hand gets no marker in that order.
- The replay behaves the same in each case.
## Tests
- Native subagent and agent tests: a task update and a late spawn after
the child finished go to `worker-1`; a held task of a subagent gets its
spawn and state in the child session after the subagent finished; a late
message chunk is still dropped; a subagent's background Bash ends after
the subagent and the client gets the terminal state in the child
session.
- Monitor: runtime, live and replay tests, also a task that names its
tool call after the tool result. A Monitor task cannot be stopped.
- Structured ids: foreground output with each marker text creates no
task; each of the four texts gives the path in both orders, also a
Windows and a POSIX path with spaces and a UNC path; no path of another
task, another tool call, `.outputs`, a relative path, a path without a
`": "` boundary, or a path across a line; two paths on one line are not
glued; a 2 MB text without `": "` parses in milliseconds; a structured
path wins. An agent test covers a subagent without `tool_use_result`,
and a replay test takes the path from the tool result.
- The new tests fail without the change. Build, lint, format and all
tests pass.
## Abandoned streamed tool calls
This part affects every client, Zed included.
**Problem.** The adapter opens a tool call at the `content_block_start`
of a streamed `tool_use`. Claude can abandon that block before it is
complete, for example for a steering message. The tool then never runs.
At end_turn the settle check reported the session failure "Claude ended
the turn without returning results for tool calls"
(`incomplete_tool_call`). A real AIR session showed a red service error
after 27 minutes of normal work, for a call that never had input.
**Fix.**
- The session records the tool calls that Claude Code sent to the tool
runner: the `tool_use` blocks of a complete assistant message, and the
tool calls of a permission request. The record is bounded and is dropped
when the call ends.
- At end_turn, an open foreground call without that record is abandoned.
The adapter closes it as failed with "Claude stopped this tool call
before it ran." and reports no session failure. The turn ends as
`end_turn`.
- An open call with that record keeps the old behaviour: the old failure
text and the `incomplete_tool_call` session failure.
- The cleanup and the cancellation checks are the same for both groups.
- The replay does not change, because the history holds no partial
`tool_use`. No scenario golden file changes, for AIR, plain or Zed.
**Tests.** A partial `tool_use` and then text ends as `end_turn` with
the neutral close. A mixed turn still fails for the unfinished call. A
steering message during a partial `tool_use` gives no failure. The
existing incomplete-tool tests now send the complete `tool_use` and keep
their failure. The new tests fail without the change.
Step 6 of opt-in ACP v2 support, option (b) as decided in review.
Stacked on #1255: both change the `initialize` line of every v2 golden
file. Once #1255 merges, I'll merge `main` into this branch so the diff
shows fork alone. Nothing changes on ACP v1.
## Why fork, then resume
The agent's v1 fork copies the transcript into a new session and returns
only its id. Until a client resumes or loads the fork, a prompt to it
fails with "Session not found", and the request's `mcpServers` go
unused. AIR, the only v1 client that forks, loads the fork right away.
The fork RFD describes the response as "the same as `session/new`", with
config options, and the request as taking `session/load`'s options. So
on v2 the client gets a session it can prompt at once:
- `v1ForkSessionRequests` (`src/v2/session.ts`) builds the v1 fork, then
a v1 resume of the fork without replay, with the request's `cwd`,
`additionalDirectories` and `mcpServers`. The request's `_meta` goes to
the fork alone.
- The response is the new `sessionId` plus the resume's `configOptions`.
The available commands follow as an update, as for `session/new` and
`session/resume`.
- `capabilities.session.fork: {}` is advertised.
Each fork starts its own Claude Code process, as `session/new` does. The
draft doesn't say whether a fork must be live, so that's recorded as
feedback for it.
## Verification
- An end-to-end test forks a session with an HTTP MCP server, checks the
forked Claude Code session resumes the copied transcript with that
server, and prompts the fork without a `session/resume`.
- Removing the resume, the MCP servers or the capability fails tests.
- Every v2 golden file changes on its `initialize` line only, which
gains `session.fork`.
- Build, lint and all 2411 tests pass.
Step 6 of opt-in ACP v2 support. Stacked on #1254: both change the
`initialize` line of every v2 golden file. GitHub retargets this PR to
`main` once #1254 merges. Nothing changes on ACP v1.
## Change
- The v2 surface serves `providers/list`, `providers/set` and
`providers/disable` through the agent's unstable v1 methods. Every
provider type is identical in v1 and v2, so requests and responses pass
through unchanged.
- The v2 `initialize` response advertises `capabilities.providers: {}`,
a constant like `capabilities.session`, since the agent serves the
methods for every client.
## Verification
- An end-to-end test lists the provider, sets a gateway and lists it,
gets `invalid_params` for an unknown provider, and disables the provider
back to the native routing.
- The handshake test checks the capability. Removing the capability or a
handler fails tests.
- Every v2 golden file changes on its `initialize` line only, which
gains `providers: {}`.
- Build, lint and all 2410 tests pass.
Found while reviewing what's left of opt-in ACP v2 support. Nothing
changes on ACP v1.
## Problem
The agent advertises `_session/steering` in the top-level `_meta` of its
v1 `initialize` response (`steering: { supported: true }`), and the v2
handshake passed that `_meta` through. The v2 surface doesn't register
`_session/steering`, so a v2 client that steered got "method not found".
## Change
The v2 `initialize` response leaves out the top-level `_meta` keys that
advertise an extension the v2 surface doesn't serve
(`UNSERVED_EXTENSION_KEYS` in `src/v2/initialize.ts`), which is
`steering` alone today. That sits next to the existing rule that keeps
the unstable `providers` capability unadvertised.
Whether v2 serves steering is left to the plan for the unstable and
extension surfaces. The protocol has no steering RFD yet, and steering
may need a different design on v2, which answers a prompt as soon as
it's accepted.
## Verification
- The handshake test checks that the v2 response has no
`_meta.steering`. The v1 handshake tests still check that v1 advertises
it.
- Every v2 golden file changes on its `initialize` line only. The line
loses its `_meta` and is otherwise identical.
- Build, lint and all 2409 tests pass.
Part of step 6 of opt-in ACP v2 support. Compaction and session notices
are both Preview features of the v2 draft that are expected to stabilize
soon, and v2 requires no client capability for either. Nothing changes
on ACP v1.
## Compaction
The agent already implements the compaction RFD for v1 clients that
declare `clientCapabilities.session.compaction`, live and on replay. A
v2 client got the legacy "Compact conversation" tool call instead,
because the v2 surface didn't declare it.
- `v1InitializeRequest` now tells the agent that every v2 client takes
compaction updates, as it already did for notices.
- `v2SessionUpdate` passes `compaction_update` and
`compaction_summary_chunk` through: their v1 and v2 shapes are
identical.
So a v2 client now gets:
- **live:** `compaction_update` `in_progress`, the summary as
`compaction_summary_chunk`, then `completed` (or `failed` with the
standard `error`);
- **on replay:** one completed `compaction_update` with the summary, at
the summary's position in the transcript. That's the materialized form
the v2 draft asks for, without the start/chunk/finish sequence.
The token counts and duration that the tool call's `rawOutput` carried
have no v2 field. Only AIR gets them, in `_meta`.
## Notices
These were already sent to every v2 client, but no test covered one on
v2. A new end-to-end test sends Claude Code's `informational` warning as
a live `notice`. The agent sends notices only live, never during replay,
which matches the draft's "SHOULD NOT be replayed".
## Not changed
**A replayed compaction's id differs from its live id.** Live, the id is
the uuid of Claude Code's `compacting` status, which the transcript
doesn't keep. Replay uses the summary message's uuid. The draft and the
RFD ask for the original, stable ids. As with replayed message ids, a
client that kept its view would see the compaction twice. Matching them
needs Claude Code to persist the live id.
## Verification
- The 3 compaction scenarios change their v2 golden files: the tool call
becomes the compaction entity. The scenario names still say `legacy`
because they're named for the v1 capability.
- Unit tests cover the pass-through and the capability. End-to-end tests
cover the live notice, and the replayed compaction between the messages
around it.
- Removing the capability or the pass-through fails tests.
- Build, lint and all 2409 tests pass.
Found while mapping commands to ACP v2 display terminals (#1240). It
affects Zed and plain ACP v1 terminal clients and the v2 surface. **AIR
is unchanged.**
## Problem
The agent reported every command's exit with a number, even when it
didn't know the code:
- **any failure as 1.** A command that exited with 2, or a denied
command that never ran, showed "Exited with code 1".
- **a backgrounded command as 0 when it launched**, though it's still
running.
- **an interrupted command as 1**, and a non-zero code that Claude Code
accepted as a success (grep with no matches) as 0.
## Change
`CommandOutput.exitCode` is now optional. `BashReporter` sets it only
when the tool result says it. I checked each source against the Claude
Code 2.1.287 binary:
- **a failure:** the `Exit code N` line that starts Claude Code's text
of a command that exited with a failing code (`rE`/`JPr` for a
`ShellError`). A failure that names none, such as a denial or a spawn
error, has no code.
- **a success:** 0, unless the structured `BashOutput` says otherwise.
Claude Code accepts a non-zero code as a success only with a
`returnCodeInterpretation`, such as grep's "No matches found" or diff's
"Files differ". An interrupted command (an Esc interrupt returns a
success with `interrupted: true`) and a backgrounded one have no code
either.
- **the code-execution block:** its `return_code`, as before.
An unknown code goes out as:
- **v1, Zed and plain clients:** `_meta.terminal_exit.exit_code: null`.
The v1 `TerminalExitStatus` allows null. Zed (`as_u64()` → `None`) and
Delta's v1 adapter (`Option<u32>`) read it as an optional code. Zed then
shows a failed command as failed, without the "Exited with code N"
tooltip, and every other exit as before. `compare.ts` allows the null
code against the origin/main baseline, a documented difference like the
`mcp` command in #1218.
- **v2:** `exitStatus` without `exitCode`, which the v2 schema treats
the same as null.
- **AIR:** the numbers it always got (1 for a failed or interrupted
command, else the code, or 0). Whether AIR accepts a null code is
unchecked, so this stays out of AIR's contract. The command fact carries
`interrupted` for that. The AIR golden files and
`docs/air-extensions.md` don't change.
## For review
- **Interrupted commands** no longer show as failed in Zed, since the
tool call completes and no code is known. Their output still ends with
"[Command was aborted before completion]".
## Not changed
- **A backgrounded command's terminal still exits at launch**, now
without a code. It could stay running until the task's
`task_notification`. That needs the task's end routed to the terminal,
also on replay, so it's a separate change.
- **Replay has no `tool_use_result`** (the same gap as replayed diffs),
so a replayed success still reports 0 when Claude Code interpreted the
code or backgrounded the command.
## Verification
A table test pins, for each kind of exit, the code Zed gets and the
number AIR gets: a success, a failure that names its code, one that
names none, an interrupted, a backgrounded, and an interpreted command.
A unit test covers the v2 `exitStatus` without a code. Removing any one
of the rules, including AIR's, fails tests. Golden files change for 3 v2
scenarios, each on its exit line only. Build, lint and all 2399 tests
pass.
## Why
#1112 removed the main-thread agent picker on the assumption that ACP
clients don't use it. That was wrong: Zed exposes the `Agent` config
option and users rely on it daily to start sessions with a custom agent
(see the feedback on #1112). It is also still the only way to pick a
main-thread agent for enterprise Claude Code setups.
Restoring it has no meaningful cost: `supportedAgents()` reads from the
already-awaited initialization response, so session creation doesn't
gain a round-trip.
## Summary
Reverts #1112:
- restore the `agent` session config option (shown only when custom
agents are configured) and custom-agent discovery
- restore live agent switching via `applyFlagSettings({ agent })`
- forward `_meta.claudeCode.options.agent` again and keep it across
context resets
- restore the exports `DEFAULT_AGENT_ID`, `AGENT_CONFIG_ID`,
`BUILTIN_AGENT_NAMES`, `discoverCustomAgents`
Conflicts with later changes on `main` (MCP command import, initial
permission mode / bypass resolution, context-window refresh after model
switch, effort settings) were resolved keeping the newer code.
## Testing
- npm run check
- npm run build
- npm run test:run (2399 passed)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Found while writing the test for #1242. It affects ACP v1 and v2.
## Problem
When Claude Code wedges after a cancel (issue #680), the force-cancel
backstop aborts the consumer's cancel signal, so the consumer settles
the turn as `cancelled`. The consumer races each next message against
that signal by adding an `abort` listener on each iteration. But a
listener added to an already-aborted signal never fires.
So if the backstop fired while the consumer was busy outside the race,
such as sending an update to a slow client, the consumer missed it. It
then waited for a message from the wedged Claude Code that never came,
and the cancelled `session/prompt` was never answered (v2: no `idle
cancelled`). Teardown aborts the same signal, which is how #1242's test
ran into it.
## Change
The race also wakes for a signal that's already aborted:
- The consumer re-arms its signal after each abort it handles, so an
aborted signal there is always an unhandled abort. It can't loop.
- A message that's already waiting still wins the race, since
`Promise.race` checks it first, so no message is dropped.
## Verification
A new turn-events test has Claude Code stream an answer and then wedge.
The client takes the chunk slowly, the user cancels, and the backstop
(10 ms in the test) fires while the consumer is still sending. The turn
ends `cancelled`; without the change it never ends. Build, lint and all
2389 tests pass.
Found while planning the permission requests of opt-in ACP v2 support
(step 5 of the v2 plan). It affects ACP v1 clients that show native
subagent sessions (`subagents` capability, or AIR's native subagent
sessions).
## Problem
For such a client, a native subagent's permission request or
AskUserQuestion goes to the subagent's child session. The agent counted
the open request on that session id, but child sessions aren't in
`sessions`, so `withPendingUserInput` counted nothing:
- **Steering:** a message steered while the subagent's request was open
didn't see it, so it went out with interrupting priority instead of
`later`, the priority used while the user has a prompt open.
- **Turn state:** the turn didn't report that it awaits the user. That's
`requires_action` in ACP v2, though the v2 surface doesn't show child
sessions today, so v2 isn't affected yet.
## Change
The request is still sent to the child session, but it now counts
against the owner session, whose turn and steering it blocks. Both paths
take the owner id: `requestPermissionFromClient` already had it
(`ownerSessionId`), and `handleAskUserQuestion` gets it next to the
session it sends the elicitation to.
## Verification
The existing "uses priority:'later' while … is pending" test gains two
cases: a subagent's permission request and a subagent's AskUserQuestion,
both sent to an announced child session. Each fails when its half of the
fix is reverted. Build, lint and all 2390 tests pass.
Found while reviewing the ACP v2 prompt lifecycle (#1226). It affects
ACP v1 and v2.
## Problem
`session/close` tears the session down: `teardownSession` cancels its
turns, wakes the consumer, and closes the query. It returned without
waiting for the cancelled turns to end, so the close could answer first:
- **v1:** the `session/prompt` response arrived after the
`session/close` response.
- **v2:** the turn's `idle cancelled` arrived after the close response.
v2's `session/close` cancels "as if `session/cancel` had been called",
then frees the session.
It happens when the consumer settles the turn late: for example, while
it's still sending an update to a slow client, or when it settles the
turn only at the end of the stream that the close ends.
## Change
`teardownSession` now returns once the turns it cancels have ended (each
turn's `completion`), for at most `TEARDOWN_TURN_END_TIMEOUT_MS` (5 s),
which it logs if a wedged consumer hits it. Its other callers get the
same order too: `session/delete`, the recreation of a session whose
parameters changed on resume or load, and `dispose()`.
## Found along the way, not changed here
The consumer's abort race adds an `abort` listener to the cancel signal
on each iteration. A listener added to an already-aborted signal never
fires, so an abort that lands while the consumer is busy, such as
sending an update, is missed. In production the close still ends the
turn, through the end of the stream, which is the path this PR waits
for. But the same gap can defeat the force-cancel backstop for a wedged
Claude Code (issue #680). That gets its own PR next.
## Verification
A new turn-events test has the client accept an answer chunk slowly,
closes the session while the consumer is still sending it, and checks
that the turn's end comes before the close returns. It fails without the
change: the close returns first. Build, lint and all 2389 tests pass.
Third part of step 4 of opt-in ACP v2 support (after #1231, #1232,
#1237): a v2 client now sees each command the agent runs in an
Agent-owned display terminal, instead of its output as a code block.
Nothing changes on ACP v1.
## Why a mapping
The agent already reports commands through the terminal extension of ACP
v1 clients, as codex-acp does:
- `_meta.terminal_info` names a terminal, which the tool call's content
references;
- `terminal_output` (a snapshot) or `terminal_output_delta` (a chunk)
carries the output;
- `terminal_exit` carries the exit.
v2 standardizes exactly this as display terminals: the same `{ type:
"terminal", terminalId }` content, plus `terminal_update` and
`terminal_output_chunk` for the terminal's state. So the v2 surface maps
the extension rather than adding new agent types. Unlike the agent's
private `_meta.claudeCode` keys, these keys are a cross-agent protocol
extension, so this maps one protocol form to another.
## Change
- `ToolCallClientCapabilities.from` reports a v2 client as taking the
extension.
- `V2Terminals` (`src/v2/terminal.ts`), one per connection, turns the
extension's keys into v2 updates, sent before the tool call report
without those keys:
- `terminal_info` → `terminal_update` naming the terminal;
- `terminal_output` → `terminal_update` with an `output` snapshot,
base64-encoded;
- `terminal_output_delta` → `terminal_output_chunk`;
- `terminal_exit` → `terminal_update` with `exitStatus`.
- **The command:** the extension names none, so the terminal gets the
command from the tool call's `rawInput` once a report carries it. The
first report of a streamed tool use has no input yet. `V2Terminals`
remembers each running command's terminal for that, and forgets it at
exit or `session/close`.
- **A report that carried only the output is dropped.** Its remaining
`_meta` (the `parentToolUseId` stamp, for a subagent's command) would
replace the tool call's stored `_meta` in v2.
- Terminal content passes through the tool call translation. A v2 client
has no client terminals, so these are the agent's only terminals.
The 8 v2 scenarios that run a command change their golden files:
`terminal_update`s in place of the code block, the terminal referenced
in the permission request, and the exit before the tool call's status.
## Not changed here
The agent reports a backgrounded command as exiting with 0 when it
launches, and a failure whose exit code it doesn't know as 1. v1
terminal clients get the same values, since the extension requires a
number. v2 can leave the exit status out, or say it's unknown, but that
needs the agent's command fact to carry it, and it affects v1 too, so
it's left for its own PR.
## Verification
- Unit tests cover output chunks and snapshots, signals, the command
arriving once, the dropped output report, a report without terminal keys
passing untouched, and forgetting a closed session.
- Each rule fails a test when removed.
- Build, lint and all 2388 tests pass.
Step 4's last session piece for opt-in ACP v2 support (after #1237,
#1238). The v2 surface now serves the whole v2 session baseline, so its
`initialize` response advertises `capabilities.session`. Until now a
conforming v2 client wouldn't call the session methods. Nothing changes
on ACP v1, and the v2 surface stays behind
`CLAUDE_AGENT_ACP_EXPERIMENTAL_V2`.
## Change
`V2_SESSION_CAPABILITIES` is a constant, since it describes what the v2
surface serves:
- **the session baseline:** `session/new`, `list`, `resume` (with replay
since #1237), `close`, `prompt`, `cancel`, and `update`;
- **`session/delete` and `additionalDirectories`.** Not `session/fork`
yet, which v1 advertises but the v2 surface doesn't serve;
- **prompt content and MCP transports,** as the agent's v1 `initialize`
lists them: images and embedded context; stdio and HTTP servers. v2 has
no SSE transport.
The top-level `_meta` (`promptQueueing`, `authStatus`) passes through as
before. `providers` stays unadvertised.
Every v2 golden file changes on its `initialize` line only.
## Release
Titled `refactor:` like the other v2 PRs, so it triggers no release on
its own. This is the point where the v2 surface becomes usable by a
conforming client, so `feat:` would also fit if you want the next
release to be a minor one.
## Verification
The handshake test pins the advertised capabilities, the schema check
validates every recorded `initialize` response, and build, lint and all
2386 tests pass.
Fixes a bug in #1237 on the opt-in ACP v2 surface. Nothing changes on
ACP v1.
## Problem
A v2 `session/resume` with `replayFrom` failed with an internal error on
any session that contains a Write:
- A replay has no approval preview and no PostToolUse hook, so the Write
reporter builds a diff from the input for every client on replay.
- That's a v1 diff. It even claims a creation, since the input doesn't
say whether the file existed.
- The v2 surface rejects v1 diffs (#1232), so the replay threw and the
resume failed.
`session-load-replay` has an Edit but no Write, so the scenario suite
didn't catch it.
## Change
A v2 client gets no diff from a Write's input, live or replayed: a v2
diff names the operation (`add` or `modify`), which the input doesn't
tell. Replayed Writes and Edits show no diff on v2 yet; building them
needs the tool's structured result, which the SDK's transcript reader
leaves out (see the follow-up discussion).
## Verification
A new test replays a transcript with a Write through a v2
`session/resume`: it failed with "Internal error" before the change and
passes after. The v1 golden files are unchanged. Build, lint and all
2386 tests pass.
Last part of step 4's session baseline for opt-in ACP v2 support (after
#1231–#1235): a v2 `session/resume` can now replay the session's
history. Nothing changes on ACP v1.
## Change
- **Routing.** v2 restores a session with `session/resume` and replays
only when `replayFrom` asks for it. v1 splits that into
`session/resume`, which never replays, and `session/load`, which replays
everything before it responds. That's exactly what v2 requires of
`replayFrom: { type: "start" }`. So `v1RestoreSessionRequest` maps the
v2 request to one of the two. A cursor that starts elsewhere is
rejected, since v1 can only replay from the start.
- **Clearing replayed messages.** The agent replays messages as chunks,
and chunks append. A v2 client may still hold a replayed message, so
before the first replayed chunk of each message, the v2 surface sends
that message with `content: []`. The draft requires that of a message
replayed as chunks. It happens only while that session replays
(`V2ClientConnection.replaying`), and the clear and the chunk go to the
connection together, so nothing comes between them.
- **Prompt ids.** The draft requires a prompt's user message to replay
under the id its `session/prompt` response returned. Since #1226 that id
is the user message's uuid, which Claude Code keeps in the transcript,
so no new code is needed. A test checks it end to end.
## Scenario suite
`session-load-replay` passes, so every scenario now passes on v2. The
status table becomes `V2_STOPS`: the scenarios that stop on v2, which is
empty today and stays as the place for a scenario v2 can't run yet. The
replay golden file shows each message cleared before its chunks, and the
`session/resume` response after the last replayed update.
A replayed Edit or Write shows no diff yet on v2: replay has no
PostToolUse hook. Building it from the transcript is the next PR.
## Verification
- **Replay test:** a live prompt, then resume with replay from a
transcript holding the uuid Claude Code saved. The replayed user message
carries the prompt response's id, each message is cleared before its
chunks, everything arrives before the response, and a live prompt
afterwards gets no clears.
- **Mutations:** each rule fails a test when removed: no clears, clears
that stay on after the replay, a replay served as a plain resume, and
one clear per id rather than per message.
- Build, lint and all 2385 tests pass.
Second part of step 5 of opt-in ACP v2 support (after #1234): the v2
surface now sends the agent's plan. Nothing changes on ACP v1.
## Change
- v1 `plan` becomes v2 `plan_update` with an items plan: `{ type:
"items", planId, entries }`. Plan entries are identical in both
versions, and a v2 items plan is replaced whole on each update, as a v1
plan is. The update's `_meta` passes through.
- **One plan id per session** (`SESSION_PLAN_ID`). v1 has one plan per
session, which every update replaces, whether it comes from TodoWrite or
from the Task tools. Several plans, such as one per subagent (a
subagent's TodoWrite replaces the session's plan today, marked by
`_meta.claudeCode.parentToolUseId`), and v2's unstable markdown plans
wait for step 6.
## Scenario suite
`todo-write` and `task-create-update` pass, with golden files. That
leaves only `session-load-replay` in the status table.
## Verification
A unit test covers the mapping; build, lint and all 2383 tests pass.
First part of step 5 of opt-in ACP v2 support (after #1231, #1232): the
v2 surface now sends the agent's permission requests. Nothing changes on
ACP v1: no v1 golden file changes.
## What differs in v2
A v1 request asks about a tool call, and clients show the tool call's
title as the prompt. A v2 request gives the prompt a required `title`
and an optional `description` of its own, apart from the tool call. It
asks about a `subject`, which can be a tool call or a command.
## Change
- **Prompt text.** The agent already computes the prompt's heading and
explanation, but only AIR gets them, in
`_meta.jetbrains.air.permission`. The agent's request type,
`AcpPermissionRequest`, now carries them as `title` and `description`
for a v2 client. It's the same rule #1232 uses for diffs: build what v1
can't express only for a v2 client. So v1 requests are byte-identical,
and the v1 scenario golden files, which record what the agent hands
`requestPermission`, don't change. The v2 surface fails on a request
without a title, so a missed site fails loudly.
- **Subject.** Always `{ type: "tool_call", toolCall }`, where
`toolCall` is the same patch a `tool_call_update` carries
(`v2ToolCallPatch`). The `command` subject would suit Bash once display
terminals exist, since its `terminalId` refers to one.
- **Approval preview.** The exact patch before an Edit or Write runs is
now built in v2 form for a v2 client, and the request shows it. This
fixed a real gap: the agent pins the preview as content the client
holds, but only AIR's request shape included it. Without the fix, a v2
client saw no diff at all, because the hook's identical patch was then
dropped as unchanged.
- **Answer.** v2's outcomes map back unchanged. An outcome v1 doesn't
know, which the draft says is no approval, becomes `cancelled`, and the
agent reads that as an aborted tool use.
- **`requires_action`** while a request is open was already in place
since #1226.
## Scenario suite
All 12 scenarios that stopped at a permission request now pass: Bash,
Edit (approved and rejected), Write, NotebookEdit, WebFetch, MCP,
network, ExitPlanMode (approved and rejected), and two subagent
scenarios. Each recording shows `requires_action`, the request, then
`running`. Now only replay and the two plan scenarios remain.
## Verification
- **End to end:** a new test drives the agent's real `canUseTool`
through a v2 client. Allow, reject, and an unknown outcome become allow,
deny, and an aborted tool use.
- **Unit tests:** the prompt's title is separate from the tool call's
(ExitPlanMode: "Ready to code?" vs "Approve Plan"), the preview shows in
the request, the v2 preview fallback works, and the title guard rejects
a missing title.
- **Mutations:** each rule fails a test when removed. Sending
`title`/`description` to every client fails 37 tests, including v1
golden files.
- Build, lint and all 2380 tests pass.
Second part of step 4 of opt-in ACP v2 support (after #1231): the v2
surface now sends file changes as v2 diffs. Nothing changes on ACP v1:
no v1 golden file changes.
## Why the agent builds them
A v2 diff names each change's operation (`add`, `modify`, …) and can
carry a `git_patch`. A v1 diff is a path with `oldText`/`newText`, and
the agent often fills those with a snippet: an Edit's
`old_string`/`new_string`, or one display hunk. So a v1 diff can't be
translated. It says neither the operation (`oldText: null` can mean a
new file, or a hunk with no old lines) nor where the lines are.
So the agent builds v2 diffs itself, at the sites that build v1 diffs
and know the change. The v2 surface creates the agent with `{ v2: true
}`, which becomes `ToolCallClientCapabilities.v2`. A v1 client can't set
it. This follows how the renderer already picks a form by capability, as
it does for AIR's `diffPatch`.
## What a v2 client gets
- **Live Edit and Write tool calls show no diff,** as a Write already
does for AIR. The input holds a snippet, or doesn't say whether the file
exists.
- **The PostToolUse hook sends the exact patch** built from the written
file. That's the existing AIR `diffPatch` path, which now produces
either form. When it can't be exact, it falls back to a patch from the
toolResponse:
- a created file: `add` with its whole text;
- an update: `modify` with the display hunks, which are fine for v2's
renderable patch;
- an update without hunks (previous content too large): the change
alone;
- an Edit with an empty `old_string`: no diff, since the response looks
the same for a created file and a filled empty one.
- **`git_patch` headers use absolute paths** without git's `a/` and `b/`
prefixes, as the draft's example does: `diff --git /p/a.ts /p/a.ts`.
## Plumbing
- The exported `toAcpNotifications` and `streamEventToAcpNotifications`
take the agent's tool call capabilities (`toolCallCapabilities`) next to
the ACP ones, which can't carry the v2 flag. External callers are
unchanged.
- The v2 diff travels through the agent as v1-typed content, with one
documented cast in `v2DiffContent`. Nothing in the agent reads inside a
diff item.
- The v2 surface passes v2 diffs through and rejects a v1 diff, so a
missed site fails loudly. That's how the scenario suite found the
notification functions on the first run.
- The approval preview stays AIR-only until v2 serves permission
requests (step 5).
## Scenario suite
`write-existing` passes, with an exact patch (golden file added).
`write-new`, `edit-with-permission` and `edit-rejected` now stop at
their permission request (step 5). No scenario stops at a diff any more.
## Verification
- Unit tests cover the absolute-path headers, the live tool calls, the
exact patch, and each fallback.
- Each new rule fails a test when removed: git-style paths in a v2
patch, the v1 snippet for a v2 Edit, the AIR path for a v2 hook result,
the notification functions ignoring the agent capabilities, and the
missing guard for an Edit that created or filled a file.
- Build, lint and all 2362 tests pass.
First part of step 4 of opt-in ACP v2 support (#1217, #1219, #1226,
#1230): the v2 surface now sends the agent's tool calls. Nothing changes
on ACP v1.
## Change
`src/v2/tool-call.ts` maps the agent's v1 tool call reports to v2:
- **One message.** v1 reports a new tool call with `tool_call` and
changes it with `tool_call_update`. v2 has one `tool_call_update` that
upserts by `toolCallId`, so both become that.
- **`null` is left out.** Both versions leave an omitted field unchanged
and replace `content` and `locations` as whole arrays. They differ on
`null`: in v1 it leaves a field unchanged (v1's `name` docs say so
explicitly), in v2 it clears it. The agent doesn't send `null` fields
today; a unit test pins the rule.
- **Content blocks pass through.** v2's content blocks are a superset of
v1's.
- **Diff and terminal content still throw**, until their own parts of
step 4.
## Scenario suite
Of the 30 scenarios that stopped at their first `tool_call`:
| Status now | Scenarios |
|---|---|
| passes | 17: `read`, `grep-and-glob`, `bash-error`, `bash-background`,
`skill`, `ask-user-question`, `memory-recall`, `permission-denied`,
`tool-progress`, `task-output-and-stop`, four subagent scenarios, the
three compaction scenarios |
| stops at a permission request (step 5) | 9 |
| stops at diff content (next part of step 4) | 4 |
The 17 new golden files are in
`src/tests/acp-scenarios/__snapshots__/v2/`.
**One new check.** In v1, a `tool_call_update` for an id the client
hasn't seen is an orphan, and clients drop it. In v2 the same message
creates a tool call. A stray update would show up as a blank tool call,
and the schema can't catch that. So every scenario recording, including
those that stop, must give each tool call a title in its first report.
It passes everywhere; making the first report lose its title fails it in
30 scenarios.
## Noted for later, not changed here
- **`_meta` becomes stored state.** In v1, a report's `_meta` belongs to
that report. In v2 it's stored on the tool call, and a concrete value
replaces it. So a v2 client keeps only the last report's
`_meta.claudeCode`: in `permission-denied`, the denial's `toolResponse`
is replaced by the result's `{ toolName }`. No v2 client reads these
keys yet.
- **v2 has a `cancelled` tool status.** v1 reports an interrupted tool
as `failed` with `_meta.claudeCode.nonExecutionKind`. Mapping it to
`cancelled` means deciding where that fact lives.
## Verification
Build, lint and all 2353 tests pass.
Last part of step 3 of opt-in ACP v2 support (#1222, #1226): the
scenario suite now also runs on the v2 surface. It's test-only.
## What
`src/tests/acp-scenarios-v2.test.ts` runs all 40 scenarios of the ACP
scenario suite the way `serveAcp` serves v2: through the protocol
router, driven by the SDK's own v2 client. Per scenario:
- `initialize`, then `session/new`, or `session/resume` with
`replayFrom` for a transcript;
- one `session/prompt` per turn, waiting for the turn's `idle`.
The agent's JSON-RPC messages are recorded off the wire, so a message
the client would reject is still recorded and checked.
Three checks:
1. **Status table.** A scenario passes when every turn ends with a stop
reason other than `_error`. Otherwise it records why it stopped: the
rejected request's error, or the failed turn's notice.
| Status today | Scenarios |
|---|---|
| passes | 7: `session-setup`, `text-and-thinking`, `goal`,
`rate-limit-and-origin`, the three async-task scenarios |
| `tool_call` not translated yet (step 4) | 30 |
| `plan` not translated yet (step 5) | 2 |
| replay not supported yet (step 4) | 1 |
Each step that translates more updates must flip its entries to
`passes`, so the table is that step's checklist.
2. **Schema.** Every recorded message, including those of scenarios that
stop, validates against the SDK's draft v2 JSON schema. The definition
is looked up from the method and side the schema itself names
(`x-method`/`x-side`). One test checks that the validator rejects a
chunk without `messageId`, so the check can't pass vacuously.
3. **Golden files** for the passing scenarios, in
`src/tests/acp-scenarios/__snapshots__/v2/`. For example,
`text-and-thinking.jsonl` shows the prompt lifecycle from #1226:
`user_message`, `running`, the `session/prompt` answer, the chunks with
their `messageId`, and `idle end_turn` with usage.
`harness.ts` gains `runScenarioV2` next to `runScenario`, sharing the
scripted Claude Code, the id normalization and the elicitation answer.
`schema.ts` gains `validateV2Message` next to `validateRecorded`.
## Testing
- Dropping `messageId` from the v2 chunk translation fails both the
schema check and the golden files of the scenarios with chunks.
- The v2 file is stable across repeated runs and takes about 1.3 s; the
full suite stays at about 4.4 s.
`npm run build`, `npm run check`, `npm run test:run` (2293 passed).
Found while reviewing #1226. It affects ACP v1 and v2.
## Problem
A turn held open for its background subagents (`Turn.deferredSettle`,
issues #864–#866) was settled `cancelled` inside `cancel()`, before the
interrupt. The followup cycle can be live at that point, writing the
subagents' summary or blocked on a permission request. The interrupt
still flushes that cycle's output, so:
- **v1:** the output arrives after `session/prompt` has answered
`cancelled`, when many clients have stopped listening.
- **v2 (#1226):** the output arrives after `idle cancelled`. The v2
draft says updates after a cancel MUST come before it.
## Change
`cancel()` now settles inline only where the consumer can't:
- **a settling turn,** blocked in its checkpoint preview, which can't
process the interrupt's trailing idle (unchanged);
- **a held turn whose session sits idle,** or has reported no state yet:
the interrupt emits nothing, so no trailing idle comes (unchanged).
**A held turn with a live followup** (`running` or `requires_action`) is
now left to the consumer, like any live turn. It ends at the interrupt's
trailing idle, or at the next prompt's echo if that idle lags, after the
output the interrupt flushed. The force-cancel backstop is armed for it
meanwhile.
Two consequences, both handled:
- **The trailing-idle debt:** the interrupted followup's result is
autonomous, so it records its trailing idle as owed. When that idle ends
the cancelled held turn, it now also pays the debt. Otherwise the debt
would absorb a later issue-#825 signal.
- **Usage:** a held turn can now be ended by the trailing idle, the echo
hand-off, stream end or the backstop, not only by `cancel()`. All of
them take the outcome from one rule, `cancelledOutcome`: a turn that
already has its result keeps that result's usage and `_meta`, as the
inline settle did (issue #844). Today the session's tally equals that
recorded outcome anyway, since followup results aren't accumulated.
Stream end already treats the stored one as authoritative.
## Testing
- **New:** `src/tests/turn-events.test.ts` cancels a held turn
mid-followup. The interrupt flushes more of the summary, then the
followup's result and idle arrive. It asserts:
- the flushed output comes before the turn ends `cancelled`;
- a wedged next turn still fails at its un-owed idle (issue #825), so no
trailer debt leaked.
- **Updated:** the two existing tests that cancel a held turn in a live
cycle (running, and blocked on a permission request) expected the prompt
to resolve at `cancel()`. Their scripts send the interrupt's trailer
only after the next prompt's echo, so the turn now ends at that echo
hand-off. They keep their issue #825 assertion, and one now also checks
the usage the turn keeps (issue #844).
- **Unchanged:** the tests for an idle hold, which settles at `cancel()`
without pre-counting a trailer, pass as before.
- **Mutations:** settling a live held turn inline again, settling an
idle one at an idle that never comes, or skipping the debt payment each
fail at least one test.
`npm run build`, `npm run check`, `npm run test:run` (2244 passed).
Found while reviewing #1226: a prompt that `session/cancel` reports as
cancelled still runs in Claude Code. It affects ACP v1 and v2.
## Problem
`cancel()` settles every queued prompt as `cancelled`, but each was
already pushed to Claude Code. Claude Code keeps queued messages through
an interrupt and runs them next. The SDK documents the interrupt
receipt's `still_queued` as "messages that WILL still run unless
cancelled first". So:
- Claude Code answers prompts the client was told were cancelled, and
they stay in the conversation, and in the model's context, for later
turns.
- The orphan accounting skips their results, and a cancelled session
drops consolidated `assistant` frames, but their streamed deltas still
reach the client after the cancel.
- In v2 (#1226), such a prompt is answered `-32800` ("not inserted"),
then its output arrives anyway.
## Change
`cancel()` now sends `cancel_async_message` for each queued message it
abandons, before the interrupt. That covers queued prompts and pending
steers. This is the pattern the SDK's own docs describe for a wrapper
that wants per-uuid control, rather than `cancel_queued` on the
interrupt, which would also sweep a prompt the client sends right after
the cancel.
- **Before the interrupt**, because once the interrupt stops the running
turn, Claude Code starts the next queued message right away.
- **Not awaited.** The SDK writes control requests to Claude Code in
call order, so the drops still arrive before the interrupt, and a wedged
CLI can't hold up the cancel, or the teardown that uses it.
- **No new accounting.** A dropped message gets a `cancelled` lifecycle
frame and is missing from the interrupt receipt's `still_queued`, and
the orphan accounting already handles both.
- **Residual window:** a message Claude Code had already dequeued, or
one the SDK hadn't written yet, still runs as an orphan, exactly as
before.
The SDK implements the request as `Query.cancelAsyncMessage` (0.3.287)
but doesn't type it, so it's looked up. A mocked query without it skips
the drop. A CLI that doesn't know the request rejects it, which is
logged.
## Testing
A new test in `src/tests/turn-events.test.ts` scripts Claude Code's
queue: a queued prompt runs after the interrupt unless it was dropped
first, as the real CLI does. It asserts:
- the drop goes out before the interrupt;
- the queued turn ends `cancelled`;
- the cancelled prompt's answer never reaches the client.
Without the drop, the test fails with the leaked answer (`chunk Answer
to a cancelled prompt` after `first ended cancelled`). Sending the drop
after the interrupt also fails it. The rest of the suite passes
unchanged. `npm run build`, `npm run check`, `npm run test:run` (2243
passed).
Part of opt-in ACP v2 support. It fixes the first issue the review of
#1226 found in the agent's turn reporting. It's independent of #1226 and
changes nothing for ACP v1.
## Problem
A turn reported `inserted` (#1222) at activation: the prompt's echo, or
for an echo-less command, its result. The echo can come after the turn's
first output; the agent already handles deltas that stream before it
(see `injectSessionEchoAt` in the tests). With #1226's v2 mapping, that
means a v2 client sees:
- a turn's output before its `user_message` and before `running`;
- `/compact`'s `session/prompt` answered only after the whole command.
## Change
On CLIs that report command lifecycles (`msg_lifecycle_v1`), the
`command_lifecycle` "started" frame now reports the turn inserted.
**Why that frame is early enough.** I read the order in the CLI bundled
with SDK 0.3.287 rather than in a live capture:
- **A command that starts a fresh turn:** the engine pushes "started"
into its lifecycle buffer at turn start (`C.push(W(w.uuid,"started"))`,
before `[engine] turn N start`). The turn's message loop then flushes
that buffer before it yields anything the turn produced
(`if(C.length>0)yield*te()`), including its echo and first
`stream_event`.
- **A prompt folded into a running turn:** its `queued_command`
attachment (the echo) is yielded before "started", so activation at the
echo already comes first and this frame changes nothing.
**One guard.** While another turn is still active, insertion still waits
for the echo. That covers a turn held for its background work, a steered
turn, and a cancelled turn awaiting its trailing idle. The echo's
hand-off ends that turn first, so a turn is never reported inserted
before the previous one ends.
**Older CLIs** without lifecycle frames keep insertion at activation.
Only the time `inserted` is reported changes. Activation, output
attribution and settlement don't, and ACP v1 ignores `inserted`.
## Testing
`src/tests/turn-events.test.ts`:
- a fresh turn whose answer streams between "started" and its echo is
reported inserted before that output;
- a prompt that starts while the turn before it is held is reported
inserted only after that turn ended.
Removing the new insertion point fails the first test, and removing the
guard fails the second. The rest of the suite passes unchanged,
including the recorded v1 scenario traffic. `npm run build`, `npm run
check`, `npm run test:run` (2231 passed).
Second part of step 3 of opt-in ACP v2 support (#1217, #1219, #1222).
The v2 surface now serves `session/prompt` and `session/cancel`. It
still doesn't advertise `capabilities.session`; that waits for tool
calls and replay (step 4).
## What
`src/v2/prompt.ts` serves a v2 prompt through the agent's turn events
(#1222), instead of translating it into a v1 prompt:
| Turn event | v2 |
|---|---|
| `inserted(messageId)` | `user_message` (the prompt as received, under
the uuid Claude Code echoed), `state_update: running`, then the answer
`{ messageId }` |
| `awaitingUser()` / `resumed()` | `requires_action` / `running` |
| `ended(outcome)` | `idle` with the stop reason, `usage`, and `_meta` |
| `ended` before insertion | `-32800`: a queued prompt that a cancel
ended |
| `failed` before insertion | the error as the JSON-RPC answer |
| `failed` after insertion | an error `notice` that shows it, then
`idle` with the custom stop reason `_error` and v1's JSON-RPC error in
`_meta.claudeCode.error` |
The last row is a gap in the draft: the idle `state_update` has no
error, its stop reasons are all non-error ones, and a notice "must not
carry … fatal-error reporting". So the failure travels on `idle` as a
`_`-prefixed custom stop reason, which the draft allows, with the error
in `_meta`; `auth_required`'s code survives there. The notice shows the
agent's own failure title ("API Error: 529 Overloaded");
`TurnEvents.failed` gains an optional `title` for it. That's going back
to the v2 draft as feedback.
- `session/cancel` maps to the v1 cancel.
- **Ordering:** the turn's state updates and the agent's own updates go
through one `ctx.notify` path in call order, so `idle` comes after the
turn's last output. The tests check the order.
- **Message chunks** (`agent_message_chunk`, `agent_thought_chunk`,
`user_message_chunk`) are translated with the `messageId` v2 requires,
and fail loudly without one.
- **Prompt content:** blocks that v1 can't express (a custom or future
type) are rejected with `-32602`. Audience roles that v1 doesn't know
are dropped from annotations, which the agent doesn't read.
- Tool call and plan updates still fail loudly until their step.
## The one change on v1
The agent built three message chunks without a `messageId`: local
command output, a result forwarded as the answer (a local-only command,
or the issue #453 fallback), and a refusal explanation. Each now carries
the uuid of the SDK message it came from, the id `messageIdForGrouping`
already uses for non-assistant messages. It's an additive field on v1.
None of the recorded v1 scenario baselines contain these chunks, so they
don't change.
## Testing
`src/tests/acp-v2.test.ts` drives the router with the SDK's v2 client,
which validates every message against the v2 schema. Its mocked Claude
Code now answers prompts: each turn echoes its prompt, then runs a
script. New tests:
- a prompt is answered with the echoed uuid while its turn still runs,
then `agent_message_chunk` and `idle end_turn` with usage;
- a queued prompt stays unanswered until the turn before it ends, then
is answered with its own echoed uuid;
- an MCP elicitation during the turn gives `requires_action`, then
`running`;
- `session/cancel` follows a real interrupt: output Claude Code sends
after it arrives before `idle cancelled`, and a queued prompt is
answered `-32800`;
- a turn whose result is an API error gives an error `notice` with the
agent's title, then `idle _error` with the error code; a missing login
keeps `auth_required`'s code;
- a queued prompt whose stream ends before insertion is answered with
the error;
- a refusal explanation carries the refusing result's uuid;
- local command output and a forwarded result each arrive with their SDK
message's uuid as `messageId`;
- a prompt with a `_acme/widget` block is rejected and never reaches
Claude Code;
- a chunk without a `messageId` fails translation, and audience roles v1
doesn't know are dropped.
Mutation check: answering at the end or at queueing instead of at
insertion, dropping `user_message`, answering a cancelled queued prompt,
dropping the failure notice or its title, ending a failure with
`end_turn` or without the error, dropping `requires_action`, removing
any of the three new `messageId`s, accepting any content block, or
swallowing a failure before insertion each fails at least one test.
`npm run build`, `npm run check`, `npm run test:run` (2240 passed).
## Not in this PR
A review of this PR found these in the agent's turn reporting and
cancellation, outside the mapping. They're tracked for their own PRs
before the v2 surface is advertised; the first three also affect v1:
- insertion is reported at the echo, which can lag the turn's first
output;
- a cancelled queued prompt's streamed output still reaches the client;
- cancelling a held turn settles it before the interrupt's remaining
output;
- `session/close` answers before the active turn's `idle cancelled`;
- whether a held turn should delay `idle` in v2 at all.
## Summary
In an ACP client, `/mcp` showed the CLI text "… Use `/mcp` in the
terminal for details." The user of an ACP client has no terminal. In SDK
mode, Claude Code also refuses `/mcp reconnect|enable|disable` with
"Reconnect, enable, and disable aren't available in this session." and
changes nothing.
- `/mcp` and `/mcp reconnect|enable|disable [<server>|all]` still run as
a normal Claude Code turn. The adapter replaces the text of the local
command, through the same hook that `/usage` uses. Prompt order, cancel,
and the provider update stay with the turn.
- For an action, the adapter runs it in that turn through the SDK
control API (`reconnectMcpServer`, `toggleMcpServer`). It shows one
result line for each server, then the status list.
- The status list comes from `query.mcpServerStatus()`. It has a summary
line and groups by status: failed, needs authentication, connecting,
connected, and disabled on one line. A failed server shows a short
cleaned error. A list is used and not a table, because a table breaks
words in narrow columns.
- Useful CLI sentences stay, for example "There's no MCP server named
…". Sentences that point to the terminal are removed.
- After an explicit reconnect, a server from the ACP session that needs
authentication starts the existing MCP OAuth flow, when the client
supports URL elicitation.
- `/mcp` is advertised with the input hint `[reconnect|enable|disable
[<server>|all]]` and is hidden on history replay.
## Other commits in this branch
This branch also carries commits of other work: skill command metadata
for AIR, a faster `session/load` replay, and the resumed permission
mode. They can move to separate PRs if preferred.
## Tests
- `src/tests/mcp-command.test.ts` (61 tests). The fake CLI answers with
the real SDK-mode texts and message shapes (synthetic assistant +
result) and does not change any status.
- A live run through the adapter with a broken and a working stdio
server: `/mcp`, `/mcp disable good`, `/mcp enable good`, `/mcp reconnect
broken`, `/mcp reconnect nosuch`.
- `compare.ts` allows one new difference against the origin/main
baseline: the `mcp` command in `available_commands_update`. The baseline
files are unchanged.
- `npm run check` and `npm run test:run` pass.
## Summary
- Bumps `@anthropic-ai/claude-agent-sdk` to 0.3.287 (Claude Code
2.1.287). No compile or test fixes were needed.
## Review notes for 0.3.287
- **WebFetch/WebSearch keep running when a steer arrives: not active for
the adapter.** In 2.1.287, a human-origin `priority: 'now'` steer no
longer cancels WebFetch or WebSearch. The call keeps running, and its
tool_result is a "[Still running…]" notice for the model with
`tool_use_result: {detachedToolCall: true}`. The steer gets its own
result, and the CLI holds back idle. When the fetch finishes, Claude
starts a follow-up turn on its own. That turn has no user message and
its result carries no uuid, and idle only arrives after it. I checked
this against the live CLI. However, the CLI only does this when no
PostToolUse, PostToolUseFailure or PostToolBatch hook applies to the
tool. The adapter's PostToolUse hook has no matcher, so it applies to
every tool, and through the adapter the steer still cancels the fetch,
as before. I also checked that end to end. Turning the new behavior on
needs three changes: limit the hook to
Write/Edit/NotebookEdit/EnterPlanMode, render the card while the call
keeps running, and close the card at the follow-up turn. That is left
for a follow-up PR.
- **No changes needed:**
- the `includePartialMessages` late or missing `message_stop` fix (the
adapter's `message_stop` handling can safely run more than once)
- the `commands_changed`-before-`init` fix
- the clearer revoked-OAuth error text (it still arrives as an
`authentication_failed` error)
- MCP `structuredContentOmitted` (the adapter doesn't read
`structuredContent`)
- the `alwaysLoad: false` semantics (unused)
- the remote-only `first_text_post_*` result fields
- `get_task_output` (in the types, but `Query` has no method for it)
- the fix for a model fallback repeating on every later message
## Test plan
- [x] `npm run build`
- [x] `npm run test:run` (2142 passed)
- [x] `npm run check`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Fixes an intermittent failure of "replays a subagent from its own
transcript file", and with it a broader leak: the unit suite was running
the real Claude CLI hundreds of times per run.
## Cause
The agent runs `claude auth status --json` in the background, without
awaiting it, on every `initialize` and prompt. The scenario suite stubs
it (`CLAUDE_CODE_EXECUTABLE=/usr/bin/false`), but other files didn't.
Logging the probe's executable over one `vitest run` showed about 670
runs of the real CLI:
| File | Real CLI runs |
|---|---|
| `acp-agent.test.ts` | 375 |
| `create-session-options.test.ts` | 14 |
| `incomplete-tools.test.ts` | 12 |
| `session-titles.test.ts` | 8 |
| `file-change-audit-integration.test.ts` | 5 |
Each run uses the machine's own Claude config, unless a test points
`CLAUDE_CONFIG_DIR` elsewhere.
That flaky test points `CLAUDE_CONFIG_DIR` at a temp dir. The CLI it
started then wrote `.claude.json`, `.claude.json.lock` and `backups/`
into that dir about a second later, after the test body had finished.
Under full-suite load, that overlapped the `rm` in the test's `finally`,
which failed with `ENOTEMPTY`. Its assertions always passed.
## Change
`vitest.config.ts` sets `CLAUDE_CODE_EXECUTABLE=/usr/bin/false` in
`test.env`:
- It's the override `claudeCliPath()` already honors, and the value the
scenario suite already uses, so production code doesn't change.
- `test.env` is the base environment, so it survives the
`vi.unstubAllEnvs()` that three files call.
- Runs that exercise the real CLI keep it: with `RUN_INTEGRATION_TESTS`
(`npm run test:integration`) or `RUN_LIVE_CAPABILITY_AUDIT=true`, the
variable is left unset.
Tests that mock `execFile` or set their own executable are unaffected.
## Evidence
- After the change, the same logging shows 0 runs of the real CLI: 688
probes run `/usr/bin/false`, and 108 hit files that already mock
`execFile`.
- The flaky test's temp dir now holds only the test's own `projects/`
after the test, with no `.claude.json`.
- Before the change, it failed in about 2 of 6 full runs on my machine.
After, 8 of 8 full runs passed.
- A full run takes about 4.8 s instead of 8.4 s.
- The variable is `/usr/bin/false` in a default run and unset with
either opt-in flag.
- `npm run build`, `npm run check`, `npm run test:run`.
First part of step 3 of opt-in ACP v2 support (step 1: #1217, step 2:
#1219). It's independent of #1219. It changes no ACP v1 behavior, apart
from one failure-only case noted below.
## Why
ACP v2 changes the prompt lifecycle. `session/prompt` is answered when
the prompt is *inserted* into the conversation, and progress and the
stop reason arrive as `state_update`s. In v1, the response comes at the
end of the turn. The agent only exposed the v1 shape: `prompt()` returns
at the end of the turn. So v2 can't be mapped from the outside.
Following the discussion on #1219, the adapter gets its own view of a
turn, and each ACP version maps to it, instead of v2 being translated
into v1.
## What
`src/turn-events.ts` defines the adapter's own turn lifecycle:
- `inserted(messageId)`: Claude Code took the prompt in as this user
message. At most once, and never after the turn ends.
- `awaitingUser()` / `resumed()`: a permission request or question is
open in the session / none is open any more.
- `ended(outcome)`: with a `TurnOutcome` (`stopReason`, `usage`,
`_meta`).
- `failed(error)`.
The agent calls these synchronously while it updates its own state, so
handlers must not throw or call back into the agent.
In the agent:
- `startTurn(params, events)` starts a turn. It resolves once the prompt
is queued for Claude Code, and rejects only if the prompt is refused
before that. Events can arrive before it resolves. `prompt()` is now the
v1 view of it: it awaits the turn's outcome.
- **inserted** is reported in `activateTurn`, the single place a queued
turn becomes active. That's when Claude Code takes the prompt in: its
echo, or for an echo-less command, its result. Two guards:
- a clear-context restart re-activates the same turn in the fresh
session, so it's reported once;
- a turn that a cancel settled during the previous turn's hand-off isn't
reported at all.
- **awaiting user / resumed** follow one session-level rule: the active
turn awaits the user exactly while any request in the session is open,
rechecked when a turn activates. A request carries no prompt id, and
Claude Code can ask for a queued prompt before the consumer reaches its
echo. So tying a request to the turn that was active when it opened
would misattribute it.
- **ended / failed** come from the existing settle points, each guarded
by `settled`, so a turn reports exactly one of them.
- `TurnOutcome` and `StopReason` are the adapter's own types. A turn's
outcome is field for field a v1 prompt response, so `PromptResponse` now
only appears at the v1 boundary.
**The one v1 difference:** a prompt that sets a goal publishes it to AIR
clients after the prompt is queued. If that notification fails,
`startTurn` now logs it rather than rejecting, because the turn is
already running. For v1 this only matters on a dead connection:
`prompt()` waits for the outcome instead of rejecting with the write
error.
## Notes for the v2 step
- A prompt sent while another turn is running is inserted only when
Claude Code takes it in, after the running turn. The open question about
queued prompts is going back to the v2 draft.
- An echo-less command (such as `/compact`) is inserted at its result,
after its output. On `msg_lifecycle_v1` CLIs, the `command_lifecycle`
"started" frame may be an earlier insertion point.
- A queued turn that a cancel ends before insertion may still be run by
Claude Code (the orphan accounting).
- Any open request counts towards `requires_action`, a background task's
included; `agentID` could refine that in the permission step.
## Testing
`src/tests/turn-events.test.ts` covers:
- a normal turn: inserted under the uuid of the message Claude Code
received, then ended;
- a permission request: awaiting user, then resumed;
- a request open from before the turn: the turn awaits the user from
insertion, through its own answered request, until the earlier one is
answered too;
- a cancel during the hand-off from the previous turn: the queued turn
ends as cancelled and is never inserted;
- cancelling while a second prompt is queued: the active turn is
inserted, then ended as cancelled, and the queued turn ends as cancelled
without being inserted;
- the stream ending with a turn queued: it fails without being inserted;
- a prompt for an unknown session: `startTurn` rejects and reports
nothing.
The existing clear-context restart test also checks that the restarted
turn is inserted once.
Each guard is checked by mutation: removing the `insertedReported`
guard, the `settled` guard, the activation recheck, or the change-only
dedupe; tying `resumed` to the request's own turn; or reporting
insertion at enqueue each fails at least one test.
The existing suite passes unchanged, including the 474 scenario tests
that compare outbound v1 traffic with recorded baselines. `npm run
build`, `npm run check`, `npm run test:run`.
Step 2 of opt-in ACP v2 support (step 1: #1217). Behind
`CLAUDE_AGENT_ACP_EXPERIMENTAL_V2=1`, the v2 surface now serves
`session/new`, `session/list`, `session/resume` (without replay),
`session/close`, `session/delete`, and `session/set_config_option`. ACP
v1 is unchanged.
`capabilities.session` is still not advertised. Under v2, advertising it
commits the agent to the whole baseline, including `session/prompt` and
`replayFrom: start` replay, and those come in later steps. Until then
these methods are served but not advertised.
## Translation
As in step 1, each v2 request becomes the v1 request that
`ClaudeAcpAgent` already serves, and each v1 response or update becomes
its v2 form (`src/v2/session.ts`, `src/v2/session-update.ts`):
- **MCP servers:** v2 requires a `type` on every transport, but the
agent recognizes a v1 stdio server by its *missing* `type`. A v2 `type:
"stdio"` config passed through as is would be silently skipped, so the
tag is dropped. The lists v1 requires (`args`, `env`, `headers`) are
defaulted. A transport v1 can't express is rejected with invalid params
instead of being left out of the session.
- **Config options:** `id` → `configId`, a select group's `group` →
`groupId`. A select value in `session/set_config_option` drops its v2
`type: "id"`, and value types v1 can't express are rejected.
- **Modes:** responses drop `modes`, and `current_mode_update` is
dropped. The agent always lists the mode as the `mode` config option,
and every mode change also reaches the client as a
`config_option_update` or in the `set_config_option` response.
- **Commands:** `available_commands_update` inputs gain `type: "text"`.
- **Notices, session info, usage:** passed through unchanged. v2 needs
no capability for notices, so a v2 client is reported to the agent as
taking them, and advisories arrive as `notice` instead of transcript
messages.
- **Elicitation:** the same in v1 and v2, so it's forwarded. MCP OAuth
at `session/new` works.
- Update kinds that aren't translated yet (messages, tool calls, plans,
extensions) throw, so a gap shows up as an error, not a missing update.
- `replayFrom` is rejected until replayed history can be translated.
Also bumps `@agentclientprotocol/sdk`:
- to 1.6.1, whose router types `closed` as always present, so `serveAcp`
drops its `closed!`;
- to 1.7.0, which type-checks the v2 extensible-union values the adapter
builds, at any depth
([typescript-sdk#268](https://github.com/agentclientprotocol/typescript-sdk/pull/268)).
The v2 translations compile unchanged. The exception is
`createElicitation`, which relays the v1 request: v1 types a property
schema's tag as any string, and an MCP server's schema passes through
unchecked, as on v1. It gets a cast with a comment saying so.
## Testing
- `src/tests/acp-v2.test.ts` drives the router with the SDK's v2 client
app, which validates every response, session update, and elicitation
against the v2 schema. New cases cover session creation with v2 MCP
servers, config options, and commands; rejected MCP transports;
list/close/resume/delete and rejected `replayFrom`; setting the mode
with no `current_mode_update` reaching the client; notice support; MCP
OAuth through URL elicitation; and unit tests for grouped options,
rejected value types, and untranslated updates. Breaking the stdio or
command-input translation makes the tests fail.
- Ran the built binary over stdio against a real Claude Code session: v2
`initialize`, `session/new`, `list`, setting the mode, and `close` all
passed the v2 client's validation, and the 52 real commands arrived with
`type: "text"` inputs. `session/new` took ~0.7 s, the same as over v1.
- `npm run build`, `npm run check`, `npm run test:run`.
First step toward optional support for the [draft ACP v2
protocol](https://agentclientprotocol.com/protocol/v2/draft/overview).
ACP v1 stays the default and is unchanged.
## What it does
Set `CLAUDE_AGENT_ACP_EXPERIMENTAL_V2=1` and the adapter routes each
connection by the protocol version the client requests in `initialize`
(via the SDK's `AgentProtocolRouter`):
- A client that requests version 2 or later gets the draft ACP v2
`initialize`, `auth/login`, and `auth/logout`, plus the
`_auth/status_update` push.
- Every other client gets ACP v1, served by the same handlers as today.
Without the variable, the adapter never builds the router and serves ACP
v1 exactly as before.
The v2 response does not advertise `capabilities.session` yet. A v2
agent that advertises it must serve the whole session baseline
(`session/new`, `list`, `resume`, `close`, `prompt`, `cancel`,
`update`), which follow-up PRs add. So for now a v2 client can
initialize and authenticate, but cannot open a session.
## How
`ClaudeAcpAgent` keeps speaking ACP v1 types. The v2 surface in
`src/v2/` is a thin translation layer around it, so v1 and v2 share one
implementation:
- `v2AgentApp` registers the v2 methods and translates each request to
the v1 request the agent already serves, and each response back. For
`initialize` that means `info`/`capabilities` instead of
`agentInfo`/`agentCapabilities`, and auth methods with `methodId` and a
required `type`, with terminal `env` as `{ name, value }` pairs. A v2
client gets no client `fs` or terminals, and boolean config options are
baseline.
- `V2ClientConnection` is the `AcpClient` of a v2 connection. It sends
the agent's v1-shaped messages in their v2 form. Only connection-level
messages can reach it so far; session-scoped methods reject until v2
sessions land.
- `v1AgentApp` is the existing v1 handler registration, pulled out of
`runAcp` so the router can reuse it. Each surface now creates the agent
of its connection in `onConnect`, because under the router the
connection opens only after `initialize` has been read. `runAcp` (a
public export) behaves as before.
Also bumps `@agentclientprotocol/sdk` to 1.6.0 (schema v2.0.0-alpha.7).
The bump changes nothing for v1: the full suite passes unchanged on it.
## Testing
- `src/tests/acp-v2.test.ts` drives the real router over in-memory
streams with the SDK's own v1 and v2 client apps (the v2 client
validates every response against the v2 schema). It covers the v1
handshake through the router, the v2 handshake, method-not-found for v2
session methods, the `_auth/status_update` push, and gateway
`auth/login` followed by `auth/logout`.
- Ran the built binary over stdio: v2 client with the flag, v1 client
with the flag, v2 client without the flag (gets the unchanged v1
response), and stdin EOF before `initialize` (exits 0).
- `npm run build`, `npm run check`, `npm run test:run`.
## Summary
- `teardownSession` sends the interrupt but does not await its reply
- `query.close()` follows at once and ends the CLI process
- `session/cancel` still awaits the interrupt reply
On a fresh session, the background `getContextUsage` request from
`refreshContextWindowInBackground` is still in flight. The SDK
serializes control requests, so the CLI answered the interrupt only
after it. With a real CLI, `session/close` took 2.5 s in an empty
directory and 3.2 s in a large repository. ACP clients with a 3 s close
timeout timed out. With this change, close answers in about 2 ms, and
the CLI process exits.
## Tests
- `npm run test:run -- src/tests/create-session-options.test.ts`
- `npx tsc --noEmit`
- `npm run check`
When a background task stops, the CLI persists a `<task-notification>`
user record. Live, the adapter hides it and handles the SDK task
lifecycle frame; replay previously showed the raw XML as a user prompt.
Replay now hides notification records and removes embedded notification
XML while preserving adjacent user text and delivered prompts with an
origin `subkind`. It restores known non-agent tasks through
`AsyncTaskRuntime`, including `killed` → `stopped` and duplicate
terminal-event suppression. Agent notifications override the launch
result's terminal state. Unknown tasks remain hidden without creating a
task card.
Task launches are recognized in `tool_use`, `server_tool_use`, and
`mcp_tool_use` blocks. Async task updates use the session that owns the
launching tool call, even when another session receives the
notification. History has no `task_started` frame, so tasks without a
persisted stop notification cannot be reconstructed.
The large Write patch test now uses a fixed clock to check patch
correctness independently of CI speed; a separate advancing-clock test
verifies the existing timeout fallback. The production 100 ms budget
remains unchanged.
Validation on current main (SDK 0.3.286):
- `npm run build`: passed.
- `npm run check`: passed.
- `env -u ANTHROPIC_BASE_URL npm run test:run`: 2119 passed, 29 skipped.
The local gateway environment otherwise changes the expected default URL
in four provider tests.
- Regression coverage includes origin/text notifications, adjacent user
text, unknown and repeated tasks, killed tasks, background Agents,
delivered subkind prompts, alternate tool block types, task session
ownership, and patch timeout fallback.
* 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))
* push renamed sessions to list subscribers and reuse one listing across pages ([#1293](https://github.com/agentclientprotocol/claude-agent-acp/issues/1293)) ([92abb8f](https://github.com/agentclientprotocol/claude-agent-acp/commit/92abb8fd4db862c465c4b3a49ea32dac0720de2c))
* add the AIR sessionIndex extension for fast session lists ([#1268](https://github.com/agentclientprotocol/claude-agent-acp/issues/1268)) ([03d84f6](https://github.com/agentclientprotocol/claude-agent-acp/commit/03d84f6663a2f561b3c59bc30e622e085b869024))
* **deps:** Bump @agentclientprotocol/sdk to 1.8.0 ([#1287](https://github.com/agentclientprotocol/claude-agent-acp/issues/1287)) ([446a721](https://github.com/agentclientprotocol/claude-agent-acp/commit/446a7217fa2654a9bcfaff9ddbfcdf2917cef024))
* Support custom instructions for agent sessions via meta ([#1177](https://github.com/agentclientprotocol/claude-agent-acp/issues/1177)) ([908c994](https://github.com/agentclientprotocol/claude-agent-acp/commit/908c994901b857918c23655670468298f6bbd33b))
### Bug Fixes
* recreate live sessions on logout when it clears a provider or gateway override ([#1278](https://github.com/agentclientprotocol/claude-agent-acp/issues/1278)) ([966be7a](https://github.com/agentclientprotocol/claude-agent-acp/commit/966be7abe60435f2f1c6379a5255ea7d533a1a20))
* report a failed turn with the error stop reason on the experimental ACP v2 surface ([#1288](https://github.com/agentclientprotocol/claude-agent-acp/issues/1288)) ([060bdb2](https://github.com/agentclientprotocol/claude-agent-acp/commit/060bdb212a9354f2dd0c88e4234a7659aeb6d27e))
* report an interrupted tool call as cancelled on the experimental ACP v2 surface ([#1289](https://github.com/agentclientprotocol/claude-agent-acp/issues/1289)) ([55c3106](https://github.com/agentclientprotocol/claude-agent-acp/commit/55c3106c60f58909af76f75df9f6c5815dcf0021))
* **deps:** Bump the minor group with 2 updates ([#1260](https://github.com/agentclientprotocol/claude-agent-acp/issues/1260)) ([0724b17](https://github.com/agentclientprotocol/claude-agent-acp/commit/0724b17c581e30b1242cfbec7bce44c21df99525))
* settle prompts folded into a task-notification cycle ([#1233](https://github.com/agentclientprotocol/claude-agent-acp/issues/1233)) ([390b0b9](https://github.com/agentclientprotocol/claude-agent-acp/commit/390b0b9f8bf0cc0aa7d9273a7b614172c916d3a6))
* answer /mcp in chat with the MCP server status and a reconnect ([#1218](https://github.com/agentclientprotocol/claude-agent-acp/issues/1218)) ([190a00f](https://github.com/agentclientprotocol/claude-agent-acp/commit/190a00f68f773315e93630464397944e645616ff))
* **deps:** Bump @anthropic-ai/claude-agent-sdk to 0.3.287 ([#1224](https://github.com/agentclientprotocol/claude-agent-acp/issues/1224)) ([ddd5f54](https://github.com/agentclientprotocol/claude-agent-acp/commit/ddd5f541879118edb18df7ff09cd6682d455c710))
### Bug Fixes
* count a subagent's permission requests as the turn waiting on the user ([#1241](https://github.com/agentclientprotocol/claude-agent-acp/issues/1241)) ([d599333](https://github.com/agentclientprotocol/claude-agent-acp/commit/d599333b722a951553c0efd40a76559b1f32e6ab))
* drop queued prompts a cancel ends from Claude Code's queue ([#1228](https://github.com/agentclientprotocol/claude-agent-acp/issues/1228)) ([d83f28a](https://github.com/agentclientprotocol/claude-agent-acp/commit/d83f28a4035c5a5e54950fd58c7b73bfc2313664))
* end a cancelled turn when the force-cancel fires during an update ([#1243](https://github.com/agentclientprotocol/claude-agent-acp/issues/1243)) ([a44c486](https://github.com/agentclientprotocol/claude-agent-acp/commit/a44c486019e98ad478c549c4902e7092f1418d0b))
* end a closed session's turns before session/close answers ([#1242](https://github.com/agentclientprotocol/claude-agent-acp/issues/1242)) ([b1d6cfc](https://github.com/agentclientprotocol/claude-agent-acp/commit/b1d6cfc6c9fc9ec397e607b68ac100f4d48745b3))
* end a held turn cancelled mid-followup at its interrupt's idle ([#1229](https://github.com/agentclientprotocol/claude-agent-acp/issues/1229)) ([d3e673b](https://github.com/agentclientprotocol/claude-agent-acp/commit/d3e673b263cf47603a35a50bb3da8b483199f690))
* excessive message/token_count API calls when probing context window ([#1258](https://github.com/agentclientprotocol/claude-agent-acp/issues/1258)) ([5598efb](https://github.com/agentclientprotocol/claude-agent-acp/commit/5598efbc8ab16720ea79f5c97db40771ad0b317b))
* replay a session with a Write on the experimental ACP v2 surface ([#1238](https://github.com/agentclientprotocol/claude-agent-acp/issues/1238)) ([22139f7](https://github.com/agentclientprotocol/claude-agent-acp/commit/22139f73c48e621752089a91fb095269493896b1))
* report a command's exit code only when the tool result says it ([#1252](https://github.com/agentclientprotocol/claude-agent-acp/issues/1252)) ([52a92be](https://github.com/agentclientprotocol/claude-agent-acp/commit/52a92be959f7f8ea90ab08d54bae6900596ab435))
* answer session/close without waiting for the interrupt reply ([#1216](https://github.com/agentclientprotocol/claude-agent-acp/issues/1216)) ([0c862e2](https://github.com/agentclientprotocol/claude-agent-acp/commit/0c862e29496bb018e7a4400697bd2315dfd5b845))
* fail turns with unfinished tools ([#1212](https://github.com/agentclientprotocol/claude-agent-acp/issues/1212)) ([7f8d547](https://github.com/agentclientprotocol/claude-agent-acp/commit/7f8d5471e24b981b129b3c9c2d6736c4ba3f8011))
* restore background task stops on replay instead of showing them as prompts ([#1205](https://github.com/agentclientprotocol/claude-agent-acp/issues/1205)) ([4ffa625](https://github.com/agentclientprotocol/claude-agent-acp/commit/4ffa625977af7e85526b9a3098f4dee5844467b4))
-`/mcp` in the chat: the MCP server status as a list. The adapter runs `/mcp reconnect`, `/mcp enable`, and `/mcp disable` through the SDK control API, because Claude Code refuses them in SDK mode. A reconnect of an ACP server that needs authentication starts MCP OAuth through URL elicitation
- Session-scoped long-running goals for AIR through the [goal extension](docs/air-extensions.md#goal) under `_meta.jetbrains.air.goal`
- Structured errors, recovery, and warnings through the opt-in [session failure extension](docs/air-extensions.md#session-failure)
- Concrete model and effort defaults through the opt-in [recommended config value extension](docs/air-extensions.md#recommended-config-values)
Subagents are exposed only after bilateral capability negotiation. Until the released ACP SDKs
preserve the draft `clientCapabilities.subagents` field, a supporting client may advertise
`nativeSubagentSessions` in `_meta.jetbrains.air.capabilities`; the adapter mirrors the capability
in its initialize response. The canonical field remains supported and takes precedence once it is
available. Without either client signal, Agent/Task lifecycle keeps its legacy ordinary ACP
tool-call representation and child interactions stay on the root session. Clients that use the
historical `_meta["subagent-transcript"]` capability or `forwardSubagentText` session option retain
the flattened child transcript behavior. See [AIR extensions](docs/air-extensions.md#native-subagent-sessions).
A client that declares `clientCapabilities.subagents`, other than JetBrains AIR, gets each Agent or
Task subagent as its own child session, following the [subagents RFD](https://github.com/agentclientprotocol/agent-client-protocol/blob/main/docs/rfds/subagents.mdx):
-`subagent_update` on the parent announces the child before any of its traffic, with the
parent's `title` and `description` for it, and reports its work state: `running`,
`requires_action` while one of its requests is open, and `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/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 and its open tool calls. 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. A child that still runs when Claude Code exits is `idle` with no stop reason.
- A child whose parent is not yet known when it ends is not exposed, since the RFD forbids
guessing its parent.
- A replay restores each child under its live session id, from its own transcript: its prompt and
each SendMessage message are `session_message`s, 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.
- A client cannot prompt, cancel, or otherwise change a child yet.
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
@@ -135,22 +137,26 @@ 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.
An extension is active only when the client declared its capability.
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 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) |
| `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) |
| `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) |
The adapter ignores `planContentDelta`.
Claude does not stream a plan, so the adapter never sends `contentDelta` (see [Plan file](#plan-file)).
@@ -164,25 +170,27 @@ Every payload goes into `_meta.jetbrains.air`, next to `version: 1`.
The adapter merges a payload into an existing `_meta` and keeps the other namespaces.
The adapter sends these keys only to an AIR client. "AIR" in the Gate column means an AIR client without a further capability.
| `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"` |
@@ -198,6 +206,20 @@ The adapter keeps them where they are.
| `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. |
## 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
The adapter keeps these Zed and upstream conventions unchanged for every client.
@@ -489,6 +511,21 @@ The plan text goes out as before in these cases:
AIR then shows the in-memory `plan` text.
A client that is not AIR always gets the whole SDK input, like upstream.
## Session hooks
The adapter accepts session hooks from any client, without a capability.
AIR puts the hooks into `_meta.claudeCode.options.settings.hooks` of `session/new` and `session/load`:
- The settings go to the flag settings tier of that session only. Other sessions and the settings files do not change.
- Claude Code runs the hooks of every settings tier, so the user, project and IDE hooks of one event all run. `src/tests/session-hooks.test.ts` checks this with the Claude Code binary.
-`CLAUDE_MODEL_CONFIG` stays in effect (see [Model configuration](model-configuration.md#precedence)).
- A `session/load` with different settings recreates the query, so it applies the new hooks.
- Claude asks for no hook trust step.
## Permission presentation
The adapter uses standard ACP permission requests and responses.
@@ -858,10 +895,17 @@ The live test only initializes the SDK. It sends no model prompt.
## Async tasks
Claude can run work in the background, for example a backgrounded Bash command, a workflow, or a monitor.
Claude can run work in the background, for example a backgrounded Bash command or a workflow.
The adapter publishes that work as async tasks when the client declares `asyncTasks`.
Without the capability, the adapter sends no async task update.
A subagent task (`local_agent`) is not an async task. Native subagent sessions report it.
A task that a `Monitor` tool call started is not an async task either.
Monitor streams its output to the model only, so the client gets nothing for it.
The SDK reports such a task as `local_bash`, so the adapter finds it by the tool of its tool call.
The `Monitor` tool call stays in the transcript.
The adapter also ignores a `local_monitor` task.
A task that a subagent tool call started goes to the subagent session.
Its spawn, progress and state updates go there also after the subagent finished.
### Updates
@@ -872,12 +916,27 @@ A subagent task (`local_agent`) is not an async task. Native subagent sessions r
-`async_task_state_update` carries `state` (`running`, `paused`, `completed`, `failed`, or `stopped`) and an optional `summary`.
- The Bash `tool_call_update` of a backgrounded command carries `_meta.jetbrains.air.asyncTasks.backgrounded: true`.
The card then shows backgrounded work instead of finished work.
Only structured data sets the marker: the `backgroundTaskId` of the tool result, a known background task of the tool call, or a `run_in_background` input.
### Task id and output path
An async task comes only from structured data.
The sources are `tool_use_result.backgroundTaskId` and the SDK `task_started`, `task_updated`, and `task_notification`.
The text of a tool result never creates a task, because a foreground command can print any text.
The output path comes from `output_file` of the SDK events when they have it.
Otherwise the adapter reads the text of the tool result of the task's own tool call.
It takes only an absolute path that ends with `tasks/<task id>.output`, followed by a period, whitespace or the end of the text.
The path starts after the nearest `": "` before that end on the same line, so it can hold spaces.
A path never holds the end of another path.
A POSIX, a Windows drive, and a UNC path count. A path across a line does not.
The tool result can come before or after the SDK names the task.
A subagent tool result has no `tool_use_result`, so this text is the only path source there.
### Tool call of a task
The adapter sends `async_task_spawned` only after it knows the tool call that started the task.
The SDK gives that id as `tool_use_id` of `task_started`, `task_progress`, or `task_notification`.
The Bash result of a backgrounded command also gives it.
The structured Bash result of a backgrounded command also gives it.
Until the id arrives, the adapter holds the task. It sends no update of a held task.
The progress and state updates of a held task follow its spawn, in their order.
A held task gets its spawn without `toolCallId` when it ends first, or when the prompt result ends the turn.
@@ -1141,8 +1200,9 @@ This section covers only the AIR bridge.
- The canonical client field is `clientCapabilities.subagents: {}`.
- Released ACP SDKs can strip that draft field.
AIR can instead declare `nativeSubagentSessions` in `_meta.jetbrains.air.capabilities`.
- Either signal enables native subagent sessions. The canonical field takes precedence when it is available.
- The agent advertises `nativeSubagentSessions` to AIR, and `agentCapabilities.sessionCapabilities.subagents` to every client.
- Either signal enables native subagent sessions, and AIR gets this earlier draft of the RFD whichever it declares.
- The agent advertises `nativeSubagentSessions` and `agentCapabilities.sessionCapabilities.subagents` to AIR.
- A client that is not AIR and declares `clientCapabilities.subagents` gets the revised RFD instead (`subagent_update`, `session_message`), see the README.
- With native sessions, the adapter sends `subagent_spawned` and `subagent_state_update`.
The child output goes to the child session. The Agent or Task tool call is not the subagent card.
-`subagent_spawned` can carry an optional `prompt`: the exact prompt of the child session.
@@ -1153,6 +1213,262 @@ This section covers only the AIR bridge.
Child interactions stay on the root session.
- A client that uses the older `_meta["subagent-transcript"]` capability or the `forwardSubagentText` session option keeps the flat child transcript.
## Session index
The session index makes `session/list` a bounded, ordered page with row metadata, and adds rename, archive and a list subscription.
### Activation
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.
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.
-`limit` is an integer of at least 1; omitted or `null` is 50. The adapter clamps it to 200 and never returns more. Another value (a string, a fraction, 0 or less) is `-32602`.
-`includeWorktrees` is a boolean, `false` when omitted or `null`. Another value is `-32602`.
Without it, the page holds the sessions that the SDK `listSessions` lists for `cwd`: the transcripts of its project directory (`cwd` with symlinks resolved, encoded as the CLI names the directory).
As in the SDK, a transcript there whose cwd (its last `relocated` cwd, else its first `cwd`) is another path that encodes to the same name (`/ws/app.v2` and `/ws/app-v2`, `/a/b` and `/a-b`) is left out only while that path still exists and resolves to another path; once it is gone, the session is listed under `cwd`.
With `true`, the page also holds the sessions of the same subdirectory of `cwd` in every other existing worktree of its repository, as the Codex TUI expands a cwd: for `/repo/packages/a`, `/wt1/packages/a` if that directory exists. For a cwd at a worktree root, that is the worktree roots.
-`archived` and `includeWorktrees` treat `null` as omitted.
-`archived` is one of three strings: `"unarchived"` lists the unarchived sessions only, `"archived"` the archived sessions only, and `"all"` both together, in one order.
Omitted or `null` is `"unarchived"`. Any other value, including a boolean, is `-32602`.
The filter applies before pagination.
- Without `cwd` the page holds the sessions of all projects.
### List response
- Rows are ordered by the last user activity descending, then by session id: `lastPromptAt`, else `updatedAt` for a row without it.
A session the agent kept working on after the last prompt does not move up, as in Codex Desktop and the AIR session tree.
With `archived: "all"`, archived and unarchived rows merge on the same key.
- Every row has `updatedAt`: the time of the last message of any kind, capped at the transcript mtime.
A rename, an archive, or another metadata record moves neither `updatedAt` nor the order.
- The cursor is opaque. It holds the position (the order key and session id of the last row) and the `cwd`, `includeWorktrees` and `archived` it was issued for; another value of any of them rejects it with `-32602`. `limit` may change from page to page.
- A deleted session is never listed, whatever `archived` says.
- A page with `nextCursor` is never empty, and a page never repeats a row of the pages before it.
- With `includeWorktrees`, the worktrees come from `<git-common-dir>/worktrees/*/gitdir`, without running git; a worktree whose directory no longer exists is left out.
A path longer than the CLI's 200-character directory name limit matches directories by name prefix, so such a directory counts only when one of its transcripts belongs to the path (its last `relocated` cwd, else its first `cwd`), as the SDK checks.
A row's `cwd` is the session's own directory, so it can be a worktree path.
- A row's `title` is the session's agent name, as AIR's native Claude provider ranks it: the top-level `agentName` of the last record that has one in the transcript's 64 KB tail, else in its 64 KB head.
Without one, it is the title the SDK reports for the session (`summary` of `getSessionInfo`): its custom title (tail, the CLI's `custom-title.json` sidecar, head) or AI title, else the last prompt, a summary or the first prompt.
When the SDK reads another copy of the session than the listed transcript, the same title is taken from the listed transcript's 64 KB head and tail.
The `[archived] ` prefix of an archived session is not part of the title (see [Archive](#archive)).
- Sidechain and subagent transcripts, transcripts without a message, and transcripts without a title are not listed.
- A session copied to several project directories of the scope is listed once, as the SDK lists it: from its most recently modified transcript that is listed.
- A transcript continued in another session (a `continued-in` record) whose successor has history is not listed, as in the SDK list.
- On macOS a project directory whose name differs from the cwd's encoding in case only is the cwd's directory, as the file system resolves it for the SDK.
- A row's `cwd` comes from the transcript (the last `relocated` record, else the first `cwd`, else the last `cwd` or one of its parents) when it encodes to the project directory name.
Otherwise it is the requested path of that directory, else the `cwd` of another session in the same directory, read further if the page does not hold one.
-`updatedAt` needs the last message and the order key the last prompt: when the 64 KB tail window lacks either, the window grows up to 4 MB to find it, reading only the new bytes each step. A last message longer than that gives the last `timestamp` of the file's last 64 KB, so title records appended after it move nothing.
The directory name is never decoded.
Each row carries `_meta.jetbrains.air` with flat row fields of the RFDs. `archived` is always there (see [Archive](#archive)); every other field is omitted when unknown:
-`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.
-`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: `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.
-`lastTurnEndedAt` is when the last turn ended.
-`cost` is the `total_cost_usd` that the SDK gave in the last result of a session this connection runs, when it is greater than 0. Any other session has no `cost`: the adapter neither reads nor estimates one from a transcript.
- A blank title is `-32602`. The adapter collapses whitespace and keeps the first 200 characters, trimmed, as the CLI does. An `[archived] ` prefix of the requested title is dropped: a rename never changes the archive state.
- The title is stored as the title records of [Archive](#archive): a `custom-title` and an `agent-name` record. An archived session keeps its `[archived] ` prefix (and the title is cut to 200 characters with it), so a rename does not unarchive it.
- A session that runs here on a CLI without the `rename_session` control request is refused with `-32600`: its CLI holds the title and would write it back.
- A session this connection runs is renamed through its CLI (`rename_session` control request), which writes the records and the sidecar of its own transcript.
A title generation in flight finishes first, and no generated title replaces the new one, also after a reload.
The adapter then sends `session_info_update { title }` with the title as stored, without the archive prefix.
Copies of the session in other project directories get the records and sidecar from the adapter.
- Any other session gets the records in every transcript of the session and the CLI's `custom-title.json` sidecar next to each (mode 0600, written atomically).
- As with the CLI's `/rename`, the records are written whether or not another Claude Code process has the session open. A transcript whose last line is incomplete gets a newline before the records.
Both are idempotent and work for a session that is not loaded.
Archiving a session stops it; unarchiving does not start it.
The archive state lives in the session title, in the format of AIR's own Claude integration, so AIR's native Claude path and this adapter read and write the same state:
- A session is archived when its name, with whitespace collapsed, starts with `[archived] `. As in AIR's native Claude provider, the name is the agent name (see [List response](#list-response)), else the custom title the SDK reports for it (`customTitle`); the agent name wins when both are there.
So an `agent-name` record with the prefix archives a session whose custom title has none, and one without the prefix unarchives a session whose custom title has it. No other title is read for the archive state.
- Archive appends the title records of `[archived] ` plus the current title without the prefix; unarchive appends those of the title without the prefix. The title is cut to the CLI's limit, its first 200 characters, trimmed; a session without a title is `Session ` plus the first 8 characters of its id.
- The title records are two lines, appended together to every transcript of the session:
```json
{"type":"custom-title","customTitle":"[archived] Fix the parser","sessionId":"…"}
{"type":"agent-name","agentName":"[archived] Fix the parser","sessionId":"…"}
```
A transcript already in the requested state gets nothing. A `custom-title.json` sidecar that the adapter finds is rewritten with the same title (also one left in the other state next to a transcript already in the requested state); the adapter creates none. The CLI of a session that runs here writes the sidecar of its own transcript, as for a rename.
The current title is the title of each transcript (see [List response](#list-response)), with the prefix only when the transcript is archived. For a session that runs here, it is the title last stored through its CLI while the last custom title of its transcript is that title; otherwise the transcript's.
Every `[archived] ` prefix of the current title is removed before one is added, so unarchive also unarchives a title stored with the prefix twice.
A new session archived before it has a title gets `[archived] Session …`, and keeps `Session …` as its name once unarchived: no title is generated for a named session.
- The list shows the title without the prefix, and so does the `session_info_update { title }` of a loaded session for an AIR client. Another client, and the `claude --resume` picker, show the title as stored, with the prefix.
- Archive of a session loaded on this connection stops it, as Codex's `thread/archive` unloads its thread:
1. it cancels the running turn, as `session/cancel` does, and sends the CLI the interrupt;
2. it retitles the session through its CLI (`rename_session`), as for [Rename](#rename): the CLI keeps the title in memory and writes it again, so only its own write cannot be overtaken. A title generation in flight finishes first, and no generated title replaces the archived one. The CLI writes the custom title before it answers and the agent name only later, so when the CLI's transcript has another agent name, the adapter appends the `agent-name` record of the new title to it at once. Its copies in other project directories get the records from the adapter as a best effort;
3. it closes the session as `session/close` does: the cancelled prompt ends with `cancelled`, and the session is no longer loaded. A later `session/prompt` fails as for a closed session; to work in it again, the client unarchives it and loads it.
4. it archives each transcript that the closed CLI left unarchived once more, with the title records.
If the retitle fails, the archive fails and the session stays loaded, with its turn cancelled.
A new session archived before the CLI wrote its transcript may have none once it is closed: it is then unknown (`-32002`) and not listed.
- Unarchive neither loads nor resumes the session. A session loaded on this connection keeps running and is retitled through its CLI, as for archive.
- Archive and unarchive of a session that is not loaded here write the title records, also when another Claude Code process has the session open, as for [Rename](#rename). That process keeps running. AIR's native Claude path stops the session's processes before it archives.
- Closing, loading or resuming a session does not change its archive state.
- After either, a session that was loaded on this connection gets `session_info_update` with `_meta.jetbrains.air.archived` set to the new state; no standard field changes. An archive sends it once the session is closed, as the last update of the session.
- Neither changes `updatedAt` or the list order: the title records are metadata.
- A session without a transcript (unknown, deleted, or removed by the CLI cleanup) is `-32002`. Unarchive never brings back a deleted session.
Only a new session that runs on this connection and has not finished a turn yet may have no transcript: it can be archived before the CLI writes one, and the CLI writes the title with it.
A loaded session whose query ended, that finished a turn, or that was loaded, resumed or forked from history needs its transcript.
- Rename, archive, unarchive and delete of one session run one at a time.
In all of the session index methods and in `session/delete`, a session id matches exactly: a transcript is the file named by the id as given.
Loading an archived session does not unarchive it.
The CLI cleanup still deletes old transcripts (`cleanupPeriodDays`), archived or not.
- `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.
- 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.
- A session is sent at most once a second per subscription. All changes of one pass go in one notification per subscription.
- Changes are tracked from the moment subscribe returns: the client subscribes first, then reads the first page with `session/list`. A row the page also holds may be sent.
- A transcript change is sent 150 ms after the last change of a burst, at most 1 s after the first; the state of a session that this connection runs is sent at once.
- Delivery is best effort: there is no resync and no sequence number. A rescan every 10 s, and one 1 s after the adapter starts watching a project directory, sends any difference it finds as a normal change. The client should still read the list now and then.
It is idempotent, and an unknown id also returns `{}`.
- A subscription lives until unsubscribe or until the connection closes; it has no time limit.
- A connection has at most 128 subscriptions. The 129th subscribe is `-32602` with `data.reason: "too_many_subscriptions"`.
- A second subscription of the same `cwd` is independent: it has its own id, its own unsubscribe and its own notifications.
- The adapter watches, without recursion, the project directories of the scope, the projects directory (for a new project directory), `<config>/sessions`, and the nearest existing parent of any of these that does not exist yet. All subscriptions of one `cwd` share that watching, which ends with the last of them.
- Subscribe reads no session row. The first subscription of a `cwd` opens the watchers, stats every transcript of the scope, and reads the registry before it returns.
- A transcript event names the file: only that transcript is read. A registry event names the record: only the sessions whose record changed get their `state` recomputed (their row is read on their first event). A session continued in a changed one is read again too.
- The rescan reads a transcript only when its stat changed or its last read failed. A session that a live process holds and that no event read gets its row, for its aging `state`, from what `session/list` already read; a worktree that came or went re-resolves only the sessions read before, by an event or by `session/list`.
- `session/list` starts or renews nothing.
### Delete and close
`session/delete`:
- 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.
`session/close` of a session that is not loaded returns `{}` for every client.
`session/load`, `session/resume`, `session/prompt`, rename, archive, unarchive and delete all work on a session that another live Claude Code process has open, for every client, as the Claude CLI does. Archive stops only a session that runs on this connection.
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)):
| `_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` |
The archive follows the semantics of the ACP "Session Archive and Unarchive" RFD (#2161): the archive state of every row, the state report in `session_info_update`, idempotency, `-32002` for unknown and deleted sessions, and an unchanged `updatedAt`.
It differs in transport, until the RFD lands in the SDK, in the list filter, and in execution:
- archive stops a session that runs on this connection, as `session/close` does, where #2161 leaves execution alone. The "Session list extensions" RFD proposes this change to #2161;
- the list parameter has three values, not two: `"unarchived"` is the RFD's `false` (and the default of both), `"all"` its `true`, and `"archived"`, the archived sessions only, has no RFD counterpart;
- the methods are `_session/archive` and `_session/unarchive`, not `session/archive` and `session/unarchive`;
- the capability is `sessionArchive` in `_meta.jetbrains.air.capabilities`, advertised together with `sessionIndex`, not `sessionCapabilities.archive`;
- 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.
## Context compaction
The adapter implements the ACP session compaction RFD for a client that declares `clientCapabilities.session.compaction`.
@@ -1240,6 +1556,8 @@ It adds this object to the `session/fork` request:
When only one stored message has the fingerprint, the adapter uses that message.
- When no message matches, the request fails with `invalidParams`.
- The fork keeps the history up to that message.
- With or without this object, the forked session runs once the response arrives, so the client can prompt it without `session/load` or `session/resume`.
A later load replays its history into it, and a later resume returns it. A load or resume with other settings (`cwd`, MCP servers, additional directories, or the session options in `_meta`) restarts it, as for any loaded session.
When an ACP caller provides`settings` via `_meta.claudeCode.options.settings` in the `sessions/create` request, `CLAUDE_MODEL_CONFIG` is ignored entirely. The env var is a deployment-level fallback for cases where the caller does not configure model settings itself.
An ACP caller can provide `settings` via `_meta.claudeCode.options.settings` in `session/new` or `session/load`, as an object or a path to a settings file. The adapter merges `CLAUDE_MODEL_CONFIG` into those settings, so a caller that only adds hooks keeps the model settings. When both set the same key, the caller wins; the merge is shallow, so a caller's `modelOverrides` replaces the env var's map.
{"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":{"claudeCode":{"toolName":"AskUserQuestion"}},"content":[],"kind":"other","name":"AskUserQuestion","sessionUpdate":"tool_call","status":"pending","title":"Asking for your input","toolCallId":"toolu_ask"}}}
{"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":{"asyncTaskId":"monitor_1","canStop":true,"description":"Watch the logs","name":"Watch the logs","sessionUpdate":"async_task_spawned","showInTranscript":true,"taskType":"monitor","toolCallId":"toolu_monitor"}}}
{"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":{"asyncTaskId":"workflow_3","canStop":true,"description":"Watch the logs","name":"Watch the logs","sessionUpdate":"async_task_spawned","showInTranscript":true,"taskType":"workflow","toolCallId":"toolu_workflow"}}}
{"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":{"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":{"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":{"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":{"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":{"jetbrains":{"air":{"contextCompaction":{"error":"Context too small","version":1}}}},"content":[{"content":{"text":"Compaction failed: Context too small","type":"text"},"type":"content"}],"sessionUpdate":"tool_call_update","status":"failed","toolCallId":"00000000-0000-4000-8000-000000000001"}}}
{"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":{"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":{"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":{"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":{"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":{"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":{"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":{"jetbrains":{"air":{"goal":{"controlMethod":"_session/goal","objective":"Ship the feature","status":"active"},"version":1}}},"sessionUpdate":"session_info_update"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [<server>|all]]"},"name":"mcp"}],"sessionUpdate":"available_commands_update"}}}
{"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":{"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":{"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":{"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":{"locations":[{"path":"<cwd>/n.ipynb"}],"sessionUpdate":"tool_call_update","title":"Edit cell in n.ipynb","toolCallId":"toolu_nb"}}}
{"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":{"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":{"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":{"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"}}}
{"kind":"loadSession","payload":{"configOptions":[{"category":"mode","currentValue":"default","description":"Session permission mode","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Always ask before making changes","name":"Manual","value":"default"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"_meta":{"jetbrains":{"air":{"kind":"plan","version":1}}},"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","id":"model","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Always ask before making changes","id":"default","name":"Manual"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Automatically accept all file edits","id":"acceptEdits","name":"Accept edits"},{"_meta":{"jetbrains":{"air":{"kind":"plan","version":1}}},"description":"Create a plan before making changes","id":"plan","name":"Plan"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Claude handles permission decisions","id":"auto","name":"Auto"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Accepts all permissions","id":"bypassPermissions","name":"Bypass permissions"}],"currentModeId":"default"},"sessionId":"<session>"}}
{"kind":"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":"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"}}}
{"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":{"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":{"capabilities":{},"name":"Background review","prompt":"Review the diff","sessionUpdate":"subagent_spawned","subagentSessionId":"a2","task":"Review the diff"}}}
{"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":{"capabilities":{},"name":"Check the logs","prompt":"Check logs","sessionUpdate":"subagent_spawned","subagentSessionId":"agent_late","task":"Check logs"}}}
{"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":{"capabilities":{},"name":"Fix the build","prompt":"Make the build pass","sessionUpdate":"subagent_spawned","subagentSessionId":"agent_n","task":"Make the build pass"}}}
{"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":{"capabilities":{},"name":"Plan the work","prompt":"Plan it","sessionUpdate":"subagent_spawned","subagentSessionId":"agent_outer","task":"Plan it"}}}
{"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":{"capabilities":{},"name":"Explore code","prompt":"Find the parser","sessionUpdate":"subagent_spawned","subagentSessionId":"agent_1","task":"Find the parser"}}}
{"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":{"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":{"capabilities":{},"name":"Summarize","prompt":"Summarize the repo","sessionUpdate":"subagent_spawned","subagentSessionId":"agent_t","task":"Summarize the repo"}}}
{"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":{"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":{"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":{"content":{"text":"Let me thin","type":"text"},"messageId":"msg_text","sessionUpdate":"agent_thought_chunk"}}}
{"kind":"sessionUpdate","payload":{"sessionId":"<session>","update":{"content":{"text":"k about it.","type":"text"},"messageId":"msg_text","sessionUpdate":"agent_thought_chunk"}}}
{"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":{"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":{"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":{"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":{"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":"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":[],"kind":"other","name":"AskUserQuestion","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Asking for your input","toolCallId":"toolu_ask"}}}
{"kind":"request","method":"elicitation/create","payload":{"message":"Which database?","mode":"form","requestedSchema":{"properties":{"question_0":{"oneOf":[{"_meta":{"_claude/askUserQuestionOption":{"preview":"CREATE TABLE t ();"}},"const":"Postgres","description":"Relational","title":"Postgres"},{"const":"Redis","description":"Key-value","title":"Redis"}],"title":"Database","type":"string"},"question_0_custom":{"description":"Type your own answer, or add a note to the option you chose above (optional).","title":"Other","type":"string"}},"type":"object"},"sessionId":"<session>","toolCallId":"toolu_ask"}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"content":{"text":"User has answered your questions: \"Which database?\"=\"Postgres\".","type":"text"},"type":"content"}],"rawOutput":"User has answered your questions: \"Which database?\"=\"Postgres\".","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_ask"}}}
{"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":"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":"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":"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":"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":"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":"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":{"compactionId":"00000000-0000-4000-8000-000000000001","error":"Context too small","sessionUpdate":"compaction_update","status":"failed"}}}
{"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":"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":"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":[{"content":{"text":"```\nThe user doesn't want to proceed with this tool use. The tool use was rejected.\n```","type":"text"},"type":"content"}],"rawOutput":"The user doesn't want to proceed with this tool use. The tool use was rejected.","sessionUpdate":"tool_call_update","status":"failed","toolCallId":"toolu_edit"}}}
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.