mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 04:30:59 +00:00
v0.95.0
869 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ebfbfbdaa9 |
chore(main): release 0.95.0 (#1391)
🤖 I have created a release *beep* *boop* --- ## [0.95.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.94.0...v0.95.0) (2026-10-23) ### Features * end a turn at its result for AIR clients with backgroundSubagents ([#1262](https://github.com/agentclientprotocol/claude-agent-acp/issues/1262)) ([v0.95.0 |
||
|
|
6ab670baae |
feat: report read-only subagent configuration (#1417)
## Summary Expose the observed configuration of RFD subagent sessions using the same config-option IDs as ordinary sessions, without granting client-initiated configuration changes. - Report model from child responses and model-switch hooks, and mode, applied reasoning effort, and agent type from child-attributed SDK hooks. - Merge partial observations into complete, ordered config snapshots; buffer early observations until truthful child announcement and serialize concurrent publications. - Advertise only each observed current value. Do not inherit parent settings or invent child Fast-mode state. - Keep child plan-mode and model-switch hooks from changing the parent's configuration. - Cover initial/changed settings, announcement ordering, publication failures, root isolation, and hook registration in both protocol versions; update v2 scenario snapshots. ## Related Protocol clarification: https://github.com/agentclientprotocol/agent-client-protocol/pull/2468 Zed client implementation: https://github.com/zed-industries/zed/pull/66058 ## Validation Passed after rebasing onto current `main`: - `npm run build` - `npm run check` - `npm run test:run -- --silent`: 3,294 passed, 30 skipped. No live Claude-to-Zed smoke test was run. Settings are reported only when observed from the SDK; availability depends on runtime support and events received.v0.94.1-preview.13 |
||
|
|
f7d6030c04 |
fix: bump the minor group across 1 directory with 2 updates (#1416)
Bumps the minor group with 2 updates in the / directory: [@opentelemetry/api](https://github.com/open-telemetry/opentelemetry-js) and [jose](https://github.com/panva/jose). Updates `@opentelemetry/api` from 1.9.1 to 1.10.0 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/open-telemetry/opentelemetry-js/releases">@opentelemetry/api's releases</a>.</em></p> <blockquote> <h2>api/v1.10.0</h2> <h2>1.10.0</h2> <h3>🚀 Features</h3> <ul> <li>feat(api): integrate the Logs API into <code>@opentelemetry/api</code> <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/4862">#4862</a> <a href="https://github.com/hectorhdzg"><code>@hectorhdzg</code></a></li> <li>feat(api): add imperative <code>context.attach()</code> for setting context across callback boundaries that <code>with()</code> cannot wrap. <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/6845">#6845</a> <a href="https://github.com/pichlermarc"><code>@pichlermarc</code></a> <ul> <li><code>attach()</code> returns a <code>Token</code> whose <code>dispose()</code> method restores the previous context. Use <code>token.dispose()</code> to detach.</li> <li><code>attach</code> is an optional method on the <code>ContextManager</code> interface; when the active context manager does not implement it, <code>context.attach()</code> logs a warning and returns a no-op token.</li> </ul> </li> <li>feat(api): add <code>AnyValue</code> type (of type <code>unknown</code>) and extend <code>Attributes</code> type values to use <code>AnyValue</code> <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/6780">#6780</a> <a href="https://github.com/trentm"><code>@trentm</code></a> <ul> <li>This is part of supporting <a href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/oteps/4485-extending-attributes-to-support-complex-values.md#how">OTEP 4485</a>.</li> <li>This is a <strong>breaking change for SDK implementations</strong>, in that they may need to support more attribute types for any APIs that accept <code>Attributes</code> or <code>AnyValue</code>. Implementors should refer to the OTEP 4485 link above for "MUST / SHOULD" requirements for complex attributes handling. See also the <a href="https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-core/src/common/attributes.ts">attributes-handling utilities provided by <code>@opentelemetry/core</code> to help</a>. For example, <code>cleanSimpleAttributes()</code> can be used to limit user-provided attributes to the set of value types supported in earlier <code>@opentelemetry/api</code> versions.</li> <li>This change is <strong>typically not a breaking change for users</strong> of <code>@opentelemetry/api</code>. There is one possible breaking case: If a TypeScript user was using the <code>Attributes</code> type in their own code and using <a href="https://www.typescriptlang.org/docs/handbook/2/narrowing.html">TypeScript Narrowing</a> to reduce an attribute value to an expected type (<a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/6579#issuecomment-4345316200">example here</a>). It is expected that this usage should be rare and, likely, limited to SDK implementations, if at all. (If this impacted your code, <a href="https://github.com/open-telemetry/opentelemetry-js/issues/new">please let us know</a>.)</li> <li><strong>Note for users of attributes:</strong> Simple attributes SHOULD be used whenever possible. Instrumentations SHOULD assume that backends do not index individual properties of complex attributes, that querying or aggregating on such properties is inefficient and complicated, and that reporting complex attributes carries higher performance overhead.</li> </ul> </li> </ul> <h3>🐛 Bug Fixes</h3> <ul> <li>fix(api): cap tracestate list-members and total length in the deprecated <code>TraceState.set()</code> so it stays in step with <code>TraceState</code> from <code>@opentelemetry/core</code> <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/6964">#6964</a> <a href="https://github.com/Zuhef"><code>@Zuhef</code></a></li> <li>fix: resolve the ESM build through the <code>default</code> export condition instead of <code>import</code>, so a resolver that activates neither <code>import</code> nor <code>require</code> still finds an entry point <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/7132">#7132</a> <a href="https://github.com/overbalance"><code>@overbalance</code></a></li> </ul> <h3>📚 Documentation</h3> <ul> <li>docs(api): complete <code>@since</code> annotations for the public API <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/7023">#7023</a> <a href="https://github.com/nabeelamjadsheikh"><code>@nabeelamjadsheikh</code></a> <ul> <li>Adds the three missing exported-type annotations and the member-level annotations for methods and properties introduced after their enclosing type.</li> <li>Corrects <code>Attributes</code>, <code>AttributeValue</code> and <code>TracerOptions</code> from <code>1.3.0</code> to <code>1.1.0</code>; all three were already importable in 1.1.0 via wildcard re-export.</li> </ul> </li> </ul> <h3>🏠 Internal</h3> <ul> <li>build: migrate the package build from <code>tsc</code> to <code>tsdown</code>, emitting dual CJS/ESM output from a single <code>dist/</code> directory <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/6293">#6293</a> <a href="https://github.com/overbalance"><code>@overbalance</code></a> <ul> <li>The public entry points are unchanged: <code>@opentelemetry/api</code> and <code>@opentelemetry/api/experimental</code>. This package already declared an <code>exports</code> map, so deep imports such as <code>@opentelemetry/api/build/src/...</code> were rejected before this change and continue to be.</li> <li>The emitted files moved out of <code>build/src</code> (CJS), <code>build/esm</code> and <code>build/esnext</code> (ESM) into <code>dist/</code>, using <code>.cjs</code>/<code>.mjs</code> extensions with matching <code>.d.cts</code>/<code>.d.mts</code> declarations. The non-standard <code>esnext</code> condition has been dropped in favour of the standard <code>import</code>/<code>require</code> conditions.</li> </ul> </li> <li>perf(api): add getGlobal fast-path <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/6956">#6956</a> <a href="https://github.com/legendecas"><code>@legendecas</code></a></li> <li>chore: remove the redundant <code>module</code> field from <code>package.json</code>; bundlers resolve the ESM build through the <code>exports</code> map <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/7130">#7130</a> <a href="https://github.com/overbalance"><code>@overbalance</code></a></li> </ul> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/open-telemetry/opentelemetry-js/blob/main/CHANGELOG.md">@opentelemetry/api's changelog</a>.</em></p> <blockquote> <h2>1.10.0</h2> <h3>🚀 (Enhancement)</h3> <ul> <li>feat(resource): create sync resource with some attributes that resolve asynchronously <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/3460">#3460</a> <a href="https://github.com/samimusallam"><code>@samimusallam</code></a></li> <li>feat: collect host id for non-cloud environments <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/3575">#3575</a> <a href="https://github.com/mwear"><code>@mwear</code></a></li> <li>feat (api-logs): separate Events API into its own package <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/3550">3550</a> <a href="https://github.com/martinkuba"><code>@martinkuba</code></a></li> <li>feat(sdk-metrics): apply binary search in histogram recording <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/3539">#3539</a> <a href="https://github.com/legendecas"><code>@legendecas</code></a></li> <li>perf(propagator-jaeger): improve deserializeSpanContext performance <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/3541">#3541</a> <a href="https://github.com/doochik"><code>@doochik</code></a></li> <li>feat: support TraceState in SamplingResult <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/3530">#3530</a> <a href="https://github.com/raphael-theriault-swi"><code>@raphael-theriault-swi</code></a></li> <li>feat(sdk-trace-base): add diagnostic logging when spans are dropped <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/3610">#3610</a> <a href="https://github.com/neoeinstein"><code>@neoeinstein</code></a></li> <li>feat: add unit to view instrument selection criteria <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/3647">#3647</a> <a href="https://github.com/jlabatut"><code>@jlabatut</code></a></li> <li>feat(tracing): expose dropped counts for attributes, events and links on span <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/3576">#3576</a> <a href="https://github.com/mohitk05"><code>@mohitk05</code></a></li> </ul> <h3>🐛 (Bug Fix)</h3> <ul> <li>fix(core): added falsy check to make otel core work with browser where webpack config had process as false or null <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/issues/3613">#3613</a> <a href="https://github.com/ravindra-dyte"><code>@ravindra-dyte</code></a></li> <li>fix(instrumentation-http): include query params in http.target <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/3646">#3646</a> <a href="https://github.com/kobi-co"><code>@kobi-co</code></a></li> </ul> <h3>📚 (Refine Doc)</h3> <ul> <li>chore: update http example <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/3651">#3651</a> <a href="https://github.com/JamieDanielson"><code>@JamieDanielson</code></a></li> </ul> <h3>🏠 (Internal)</h3> <ul> <li>chore(exporter-jaeger): deprecate jaeger exporter <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/3585">#3585</a> <a href="https://github.com/pichlermarc"><code>@pichlermarc</code></a></li> <li>fix(sdk-metrics): fix flaky LastValueAggregator test by using fake timer <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/3587">#3587</a> <a href="https://github.com/pichlermarc"><code>@pichlermarc</code></a></li> <li>fix(test): fix failing tests by preventing source-map generation <a href="https://redirect.github.com/open-telemetry/opentelemetry-js/pull/3642">#3642</a> <a href="https://github.com/pichlermarc"><code>@pichlermarc</code></a></li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="v0.94.1-preview.12 |
||
|
|
28a09b1008 |
chore: remove the unused ts-node devDependency (#1414)
Removes `ts-node` 10.9.2 from devDependencies. Nothing in the repo uses it: no script, config or test references it. `npm run build` uses `tsc`, tests run through vitest, and `npm run example:simple-client` uses `node --experimental-strip-types`. Verified: `npm run build`, `npm run check`, `npm run test:run`. |
||
|
|
a1521b6196 |
feat: update to Claude Code 2.1.309 (#1413)
Updates `@anthropic-ai/claude-agent-sdk` from 0.3.303 to **0.3.309** (Claude Code **2.1.309**), `@agentclientprotocol/sdk` 2.0.0 → 2.0.1, `@anthropic-ai/sdk` 0.135.0 → 1.1.0 and typescript-eslint 8.71.2 → 8.72.0. TypeScript stays on 6.0.3. typescript-eslint requires `typescript <6.1.0` and has no TS 7 support yet (typescript-eslint/typescript-eslint#10940), so we wait for that rather than running two TypeScript versions side by side. ## Uses new SDK features - **`/usage` recommendations (0.3.305):** our structured `/usage` Markdown now lists Claude Code's per-window suggestions (`- **41%** Disable my_server · /mcp`) above the MCP server table, in the order Claude Code sends them. `fix` is not rendered: its messages must be queued at priority `later` with `client_composed` set, which an ACP prompt cannot express. A malformed recommendation makes the response fail validation, so we fall back to Claude Code's own `/usage` text. - **`wakeup_source` on `command_lifecycle` "started" (0.3.307):** when a ScheduleWakeup or another scheduled task starts a turn with no prompt open, the async-task (AIR) "Claude is working" task now says `A scheduled wakeup fired (set for <UTC time>).` or `A scheduled task fired.` instead of `Claude Code started a turn without a prompt.` It works whether the frame arrives before or after `session_state_changed: running`; older CLIs and unknown values behave as before. ## Fixes for changes in the new CLI - **Fork transcripts (2.1.307):** the placeholder `tool_result` text in a fork's copied prefix changed (`Fork started — that fork is you.` / `This is the parent's call, not yours. …`). `stripForkPrefix` matches only the directive text, so behaviour is unchanged; its doc comment and the replay fixture now describe both formats. ## Behaviour changes ACP users may notice - **2.1.304:** tool search is on by default behind a custom `ANTHROPIC_BASE_URL` (our `providers/set` gateway sessions), and a gateway that rejects it is retried without it (2.1.305). - **2.1.304:** `rm -r` of a mounted drive or a top-level folder on it (`/Volumes/…`, `/media/…`, `/mnt/c/…`) now asks for permission, even in bypass mode, without an always-allow option. `CLAUDE_CODE_DISABLE_MOUNTED_DRIVE_RM_PROMPT` turns it off. - **2.1.309:** with `sandbox.enabled` but no runnable sandbox, commands in `sandbox.excludedCommands` now ask before writing to the sandbox's write-list paths, and the unsandboxed fallback is refused. - **2.1.304:** `allowedMcpServers` no longer filters `managed-mcp.json` stdio servers that use `${VAR}` only in `env`. ## Upstream fixes this picks up - Model switches, prompts and interrupts no longer wait behind our background `getContextUsage` call (2.1.304); `setModel()` waits up to 15 s for the API to confirm a model (2.1.307). - A PreToolUse hook's deny reason is no longer replaced by a timeout of our `Edit|Write` preview callback hook (2.1.304). - Host-supplied `managedSettings` no longer drop `disableAllHooks`, `disableSkillShellExecution` and empty HTTP hook lists (0.3.309). - Gateway fixes: `context_management` rejections are retried without the field (2.1.304); no `prompt-caching-scope` header behind custom gateways (2.1.307). - A disabled MCP server is no longer reconnected by a later permission prompt in SDK sessions; HTTP MCP tool calls are no longer sent twice when the connection drops (2.1.307/2.1.309). - Bash permission and sandbox hardening (2.1.309), fewer "prompt is too long" and autocompact-thrashing errors, and faster resume of large compacted sessions. ## Checked, no change needed - **ACP SDK 2.0.1** only changes the client-side `createHttpStream` transport, which we don't use. - **`@anthropic-ai/sdk` 1.x** (API version 2026-10-21, Bearer auth, sampling params removed): we only use its types, none of which changed in a way that affects us. Neither our `dist/` nor the Agent SDK runtime imports it, and the bundled CLI carries its own client. It is Apache-2.0 from 1.0.0 (previously MIT). - **`sed -i` (2.1.309)** runs as plain Bash; SDK sessions already did, since `_simulatedSedEdit` only exists in the TUI preview. - The remaining type changes (`liveReply`, context-usage row `id`s, `pluginId`/`path` on agents and skills, new settings keys) are additive and unused. Verified: `npm run build`, `npm run check`, `npm run test:run` (3277 passed, 30 skipped), and a typecheck of `examples/`.v0.94.1-preview.11 |
||
|
|
0fe22ae5a1 |
fix: bump fast-uri from 3.1.8 to 3.1.9 (#1410)
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.8 to 3.1.9. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/fastify/fast-uri/releases">fast-uri's releases</a>.</em></p> <blockquote> <h2>v3.1.9</h2> <h2>Security</h2> <p>This security release fixes four URI parsing and normalization vulnerabilities in fast-uri 3.x. Applications that process untrusted URI input should upgrade to v3.1.9.</p> <ul> <li><strong>Medium — URI scheme spoofing through percent-encoded schemes</strong> (<a href="https://github.com/fastify/fast-uri/security/advisories/GHSA-w74x-v6f9-j3xg">GHSA-w74x-v6f9-j3xg</a>, CVE-2026-110306): percent-encoded scheme input is now rejected instead of being decoded into a live scheme.</li> <li><strong>Medium — Host confusion during domain-host canonicalization</strong> (<a href="https://github.com/fastify/fast-uri/security/advisories/GHSA-wjr9-j795-jxcr">GHSA-wjr9-j795-jxcr</a>, CVE-2026-110180): malformed domain hosts containing ambiguous delimiters or stripped control characters are now rejected instead of being silently rewritten.</li> <li><strong>Low — Incorrect relative-reference normalization</strong> (<a href="https://github.com/fastify/fast-uri/security/advisories/GHSA-xhq7-xmqj-769h">GHSA-xhq7-xmqj-769h</a>, CVE-2026-110095): dot-segment removal now preserves the resolution semantics of relative references.</li> <li><strong>Low — Scheme injection in suffix references</strong> (<a href="https://github.com/fastify/fast-uri/security/advisories/GHSA-333x-w4r6-6crp">GHSA-333x-w4r6-6crp</a>, CVE-2026-110725): first-segment colons in path-only suffix references are now escaped so the output is not reparsed as a scheme.</li> </ul> <p>Upgrade with:</p> <pre lang="sh"><code>npm install fast-uri@3.1.9 </code></pre> <p>Thanks to <a href="https://github.com/zekth"><code>@zekth</code></a> for reporting the scheme, host, and relative-reference issues, and to <a href="https://github.com/UlisesGascon"><code>@UlisesGascon</code></a> for finding the suffix-reference issue and reviewing the remediations.</p> <h2>What's Changed</h2> <ul> <li>ci: enable trusted publishing releases (v3.x) by <a href="https://github.com/mcollina"><code>@mcollina</code></a> in <a href="https://redirect.github.com/fastify/fast-uri/pull/229">fastify/fast-uri#229</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/fastify/fast-uri/compare/v3.1.8...v3.1.9">https://github.com/fastify/fast-uri/compare/v3.1.8...v3.1.9</a></p> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="v0.94.1-preview.10 |
||
|
|
59e292e29f |
feat: report RFD delegations as linked session messages (#1405)
## Summary For a client on the subagents RFD (every ACP v2 client, and v1 clients that declare `subagents`), session messages now replace the Agent/Task call that launches a child. The parent no longer sees that tool call. AIR's form is unchanged. - **Prompt.** The launch's prompt is a `session_message` in both transcripts: the parent's outgoing view and the child's incoming one. Both use the launch's tool use id, and each names the other with `recipientMessageId` / `senderMessageId` from agentclientprotocol/agent-client-protocol#2465. - **Answer.** A foreground child's answer, from the launch's result, is the child's `session_message` to the parent (`<launch id>:reply`). It uses the result as `AgentReporter` renders it, so the model-facing `agentId: …` line and `<usage>` block are gone. A failure or cancellation comes from the child's `idle` stop reason, which it already reported. A background launch's result gets no reply. - **Hand-back mode.** Claude Code (seen with 2.1.303) can have the child hand its report back with a `SubagentHandback` call; the Agent result then only says that the report "was delivered to you as a message". That call is reported as the child's message to the parent, in both transcripts and linked the same way, and the launch's reply is skipped for that run. A failed hand-back delivered nothing, so it stays a tool call of the child. - **Unexposed work.** A launch whose child is never exposed is shown as a tool call after all, with its final status (`unexposedControlFallback`, the former `failedControlFallback`). - **Hidden launches by id.** The adapter's own report that closes an unfinished call (for example at a forced cancel) carries no `toolName`, so the runtime tracks hidden launches by id to keep that report from reaching the client for a call it never saw. - **Replay** sends the same messages from the child's transcript: the launch's prompt, the reply, and hand-backs. A launch without Claude Code's metadata file keeps its tool call and the missing-history notice. Replay can't tell a child's runs apart, so any hand-back in its transcript suppresses its launch's reply. - **SendMessage** calls stay tool calls of their sender, and the child gets the incoming view without references, as before. - README's subagent section describes the new behavior. Do not merge before agentclientprotocol/agent-client-protocol#2465 settles the field names: a merge to `main` publishes a preview right away. Clients on the published schema ignore the two fields (no `deny_unknown_fields` in the Rust schema; the scenario validation with the SDK's zod schema passes). ## Testing - `npm run build`, `npm run check` and `npm run test:run` pass (57 files, 4 skipped as before). - The six v2 `subagent-*.jsonl` scenario snapshots changed: the launch's tool-call updates are gone, the parent gets the outgoing view, the child's view carries `senderMessageId`, and foreground results became `:reply` messages. AIR snapshots are unchanged. - New unit tests cover hidden launches, nested launches, replies, failed and background launches, unexposed launches with both final statuses, the close report without a tool name, and hand-back success and failure, live and on replay. - A real run: the built adapter driven over v2 stdio with Claude Code, one foreground subagent asked to answer PONG, then `session/resume` with replay in a fresh process. Live and replay both showed the linked prompt (parent → child), the linked hand-back "PONG" (child → parent), and no Agent tool call.v0.94.1-preview.9 |
||
|
|
50cd76e78d |
fix: publish the session title as soon as the CLI has it (#1406)
## Summary Session titles showed up only after the first turn ended. With background subagents, that could be minutes or hours later. Now the adapter asks for the title when the prompt is sent to the CLI. In a live run the title arrived **0.8s** after the prompt, compared with **17.9s** (at turn end) on `main`. ## Why - **The CLI already titles SDK sessions.** The bundled CLI (2.1.303) titles a new session from its first prompt within seconds and stores it as an `ai-title` transcript entry. `getSessionInfo` returns it as `customTitle`. The header comment in `session-titles.ts` said the CLI never titles SDK sessions; that is out of date. The CLI's off switch only applies to the `sdk-cli` entrypoint without `CLAUDE_AGENT_SDK_VERSION`, and the TS SDK sets `sdk-ts` and that variable. - **No message carries an AI title.** The CLI's internal `system/session_title_changed` covers only `/rename`-style names, and its schema says so. - **The adapter only checked at `idle`.** `SessionTitles.onTurnEnd` ran at `session_state_changed: idle`, which comes after the whole turn. Since CLI 2.1.270 it also waits for background subagents to finish. In the common case, `onTurnEnd` just picked up the title that had been on disk for the whole turn. ## Change - **New `SessionTitles.onPromptQueued`**, called right after the prompt is pushed to the SDK. Until the session has a title, it: 1. reads the session file, and publishes a stored title if there is one (a resumed session, or a `/rename`); 2. otherwise sends `generate_session_title` with the prompt text only, in the background, and publishes the result. - **No second model call.** The CLI runs this request alongside the turn instead of queuing it behind it. Its handler returns the stored custom title, then the AI title, and otherwise waits for the CLI's own title call already in flight. - **One try per prompt, like the CLI.** After a prompt-time request, the turn end does not ask again. If nothing came back, it publishes the stored summary as before, and the next prompt tries again. - **Unchanged:** - the turn end still titles a prompt too short to title from (under 10 characters, like "hi") from the prompt plus the reply; - the turn end still picks up a later `/rename`; - a title the client set still wins. ## Verification - `npm run check` and `npm run test:run` pass (3275 passed, 30 skipped). - New tests: - the title is published while the first turn is still running; - the raw prompt is not published at turn end while a title is still pending; - a short "hi" prompt is titled at turn end from the prompt and the reply. - The latch-release test now pins one request per prompt. Removing the new call site makes two of the tests fail. - Live check: one real text-only turn against this branch and against `main`, both on the bundled CLI 2.1.303, timing `session_info_update` against the prompt response: | build | title published | prompt response | | --- | --- | --- | | this branch | 0.8s | 17.1s | | `main` | 17.9s | 17.9s | Both runs got the same title, and both transcripts contain the same two `ai-title` entries. So the change adds no title generation; the CLI itself writes the entry twice. ## Risks - **Undocumented CLI behaviour.** This depends on the CLI answering `generate_session_title` mid-turn and returning the title it is already generating, which `sdk.d.ts` does not document. If a future CLI changes that, the worst case is one extra small-model call per session. The latch still limits it to one success. - **Titles now arrive mid-turn.** Clients that expected `session_info_update` only between turns should be checked. - **Steering is not covered.** Messages injected with `_session/steering` don't trigger a title request.v0.94.1-preview.8 |
||
|
|
aa62f44886 |
ci: resolve latest Node LTS patches before using runner cache (#1404)
## Summary - Enable `check-latest: true` for all three Node setup steps: CI, stable publishing and preview publishing. - Preserve the existing `lts/*` policy, action pins, npm settings, package version and `engines.node` requirement. ## Why The latest sampled [CI](https://github.com/agentclientprotocol/claude-agent-acp/actions/runs/39865544394) and [preview publishing](https://github.com/agentclientprotocol/claude-agent-acp/actions/runs/39865544489) runs both selected cached Node **24.21.0**. That predates **24.22.1**, the fixed Node 24 release for the [October 21 security updates](https://nodejs.org/en/blog/vulnerability/october-2026-security-releases), including CVE-2026-111497. ## Validation The same checks passed before and after: - Baseline on local Node **24.22.0**: `npm ci && npm run check && npm run build && npm run test:run`. - After on official Node **24.22.1** (archive SHA-256 verified): `npm ci && npm run check && npm run build && npm run test:run`. - Both runs: **3,272 passed, 30 skipped**; the skipped/integration coverage remains unverified. - Parsed workflow configuration to verify all three setup steps keep `lts/*` and enable `check-latest`. - Tests ran natively on macOS; GitHub runner selection remains to be confirmed by PR CI. No publish or release commands were run. ## Risk / rollout Low-risk CI-only runtime freshness change. Verify the next CI/stable/preview job's setup-node log resolves at least **24.22.1** for Node 24. Reverting restores cache-first selection, which can restore an unpatched runtime; it is not a mitigation. This patches the build/publishing environment, not end users' separately installed Node runtimes. No application-code or release-version changes are included.v0.94.1-preview.7 |
||
|
|
9b3a5c891c |
feat: end a turn at its result for AIR clients with backgroundSubagents (#1262)
## What This PR adds the AIR client capability `backgroundSubagents`. A client declares it in `initialize.clientCapabilities._meta.jetbrains.air.capabilities`. With the capability, an ACP v1 prompt turn ends at its result. This is also true while background subagents of the turn still run, including a subagent that SendMessage resumed. Subagent updates, permission requests and elicitations then go out after the prompt response. Their routing does not change. The agent advertises the capability only to ACP v1 AIR clients. ## Why The hold of #864/#866 keeps `session/prompt` open until the subagents finish. The Claude CLI ends the model turn at `result`, and only `idle` waits for the background agents. In AIR the hold hides the end of the answer, and the next user message steers into an old turn. One SendMessage-resumed subagent kept a turn open for about two hours. ## Unchanged - A client without the capability keeps the hold exactly. So #866 stays fixed for clients that stop listening at the prompt response. - ACP v2 keeps the hold too. ## Related changes - Out of a turn, the ExitPlanMode request of such a client does not offer the clear-context option. The fresh query must continue an ACP turn, and there is none. - An ExitPlanMode interruption of an autonomous cycle with no held turn now clears its markers. ## Docs `docs/air-extensions.md` has a new "Background subagents" section. ## Tests New capability cases: - a fresh subagent; - a SendMessage-resumed subagent; - a permission request after the prompt response, with the `parentToolUseId` attribution; - an autonomous followup that does not end the queued next prompt; - ExitPlanMode out of a turn, with and without a reset; - the exclusion of a running subagent from the incomplete-tool check. The existing tests cover the path without the capability. When the gate is disabled, 7 of the 8 new cases fail. The eighth is the in-turn control case and passes in both configurations. `npm run build`, `npm run check` and `npm run test:run` pass. The exception is 4 `providers.test.ts` cases. They depend on the environment (a local proxy `baseUrl`) and fail on `main` too.v0.94.1-preview.6 |
||
|
|
ecaa2c935b |
fix: bump postcss from 8.5.30 to 8.5.31 in the minor group (#1403)
Bumps the minor group with 1 update: [postcss](https://github.com/postcss/postcss). Updates `postcss` from 8.5.30 to 8.5.31 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/postcss/postcss/releases">postcss's releases</a>.</em></p> <blockquote> <h2>8.5.31</h2> <ul> <li>Fix better source map on <code></style></code> escaping (by <a href="https://github.com/firefoxic"><code>@firefoxic</code></a>).</li> </ul> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/postcss/postcss/blob/main/CHANGELOG.md">postcss's changelog</a>.</em></p> <blockquote> <h2>8.5.31</h2> <ul> <li>Fix better source map on <code></style></code> escaping (by <a href="https://github.com/firefoxic"><code>@firefoxic</code></a>).</li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="v0.94.1-preview.5 |
||
|
|
5b11f470b1 |
feat: show messages typed in other Remote Control clients (#1371)
A message typed in claude.ai or the Claude mobile app while Remote
Control is on never appeared in the client that hosts the session, even
though Claude's reply to it did. This makes both sides of that
conversation show up.
Two causes, one commit each:
- **Messages from other clients were dropped.** Claude Code echoes them
to the adapter as a replayed user message with `origin: { kind: "human"
}` and a uuid the adapter never created. That matched no turn, so it
fell into the "unrelated replay" branch and was ignored. Human-origin,
top-level, non-synthetic messages that match no turn are now forwarded
as `user_message_chunk` (mapped to `user_message` on ACP v2). The echoes
of the adapter's own prompts still match their turn first and stay out
of the feed.
- **The stream reader was not running after `/remote-control`.** The
reader that forwards Claude Code's output starts with the first prompt,
and `/remote-control` answers without a turn. In a thread where it was
the first thing run, nothing read the stream, so neither the phone's
message nor Claude's reply reached the client. The command now starts
the reader. This also fixes the reply side for #1197.
## Testing
- `npm run check`, `npm run build` and `npm run test:run` pass, with new
tests in `remote-user-messages.test.ts` and `remote-control.test.ts`.
The tests for both fixes fail without them.
- Ran the built adapter as a custom agent in Zed: after
`/remote-control`, a message sent from the Claude iOS app showed up in
the Zed thread along with Claude's reply.
- Not tested: messages with images or attachments, and whether resuming
a session can replay old human messages through the same path.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
v0.94.1-preview.4
|
||
|
|
e9f986b8a9 |
fix: IJAI-3744 keep a session running while its background tasks are open (#1399)
## Summary A Claude Code session can start a background shell command or a background sub-agent, and then end its turn. When a task ends, Claude Code wakes the session for a short turn. The session index reported `idle` at each turn end, so a client saw one completion for each task. Now the index finds the open background tasks in the transcript. A turn end with an open task stays `running`, also after 10 minutes. The session gets one `idle`, when the last task ends. The rules are the same as in the native Claude provider of AIR (IJAI-3744). ## Rules - A task opens with a tool result that has `backgroundTaskId`, or `isAsync` with an `agentId`. - A task closes with its `<task-notification>` (a user record or a queue `remove`), or with a `TaskStop` result. - The wait ends when no live process holds the session, or when the registry status is `idle`. - A task can start before the tail window. A later scan applies the tail on top of the earlier task set. When it cannot, the index reads the whole transcript, but only for a finished turn of a live session. ## Not in this PR - A background task that a sub-agent starts. Its start record is only in the transcript of the sub-agent. - A session that this connection runs. Its state comes from the SDK. ## Tests - `background-tasks.test.ts`: the record orders of the native provider tests, in a small transcript and in one longer than the tail window. The tests also cover a task before the tail window, the end of the wait, and the count of whole-file reads. - A real check with Claude Code 2.1.306 in a terminal. With two background shells or two background sub-agents, `main` gave three `idle` and this change gave one. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>v0.94.1-preview.3 |
||
|
|
4d378f93f3 |
feat: serve experimental ACP v2 (#1400)
## Summary The draft ACP v2 is no longer opt-in. The adapter always connects through the SDK's protocol router (as codex-acp does), so a client that asks for protocol version 2 or later in `initialize` gets v2, and every other client gets v1 as before. v2 is still experimental and follows the draft as it changes. - Removes `CLAUDE_AGENT_ACP_EXPERIMENTAL_V2`, which only turned on what is now the default. - Adds `CLAUDE_AGENT_ACP_PROTOCOL` to limit the adapter to one version: - unset, empty, or `auto`: both, picked by the client's `initialize`. - `v1`: a client that asks for v2 is negotiated down to v1. This is the escape hatch if a client's v2 handling breaks. - `v2`: a v1 client's `initialize` is rejected with `-32022 Unsupported protocol version`. - Any other value fails at startup, so a typo like `CLAUDE_AGENT_ACP_PROTOCOL=1` doesn't silently serve both. - Fixes the comment in `src/acp-subagents.ts` that said v2 clients don't get subagents. Note: a client that already sends `protocolVersion >= 2` switches to the v2 surface as soon as this lands on `main`, which publishes a preview. ## Testing - New routing tests: down-negotiation to v1, rejecting v1 when limited to v2, env parsing. - `npm run check`, `npm run build`, `npm run test:run` pass. - Ran the built binary over stdio with each setting: default → v2 for a v2 client, `v1` → protocol version 1, `v2` → `-32022` for a v1 client, invalid value → exits with an error at startup. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>v0.94.1-preview.2 |
||
|
|
a1d0eeeaad |
fix: publish turns that Claude Code starts after end_turn as an async task in v1 (#1390)
## Motivation
Claude Code can start a turn of its own after a prompt has ended, for
example to answer the notification of a finished background task. ACP v1
has no session state, and the client already got `end_turn` for its last
prompt. The output of that turn just keeps arriving as session updates,
and the client has no signal that the agent is working again, or when it
stops.
## Change
The adapter now publishes such a turn as an async task (AIR async tasks
extension) with the id `autonomous-turn-<uuid>`, the name `Claude is
working`, `taskType: "turn"` and `showInTranscript: false`. Its
description is the summary of the last task notification, when there is
one.
- **Start:** the SDK moves to `running` while no prompt is active or
queued. From CLI 2.1.270 the state stays `running` while background
agents run, so the task also starts at the first root output with no
prompt open.
- **End:** the task ends `completed` at the turn's result or at `idle`,
and `failed` at an error result. A coalesced placeholder result
(`num_turns: 0`) does not end it.
- **Folded prompt:** a prompt that the turn takes in ends the task,
because that prompt owns the turn now.
- **Stop:** `_session/async_task/stop` on this task interrupts the turn,
since the SDK has no task to stop. It returns `{ "stopped": false }`
while a prompt waits for its turn, because the interrupt would drop that
prompt.
- **Lifecycle:** the SDK task list does not end the task, because the
SDK does not know about it. Shutdown fails it like other open tasks.
Clients without the async tasks capability get nothing new. The behavior
is documented in `docs/air-extensions.md` under "Autonomous turn".
## Tests
`acp-agent.test.ts` covers the start at `running` and at the first
output, the end at the result, the folded prompt, stop as an interrupt,
and a client without async tasks. `async-tasks.test.ts` covers the SDK
task list reconciliation and shutdown.
v0.94.1-preview.1
|
||
|
|
7bcbfc7a5c |
chore(main): release 0.94.0 (#1381)
🤖 I have created a release *beep* *boop* --- ## [0.94.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.93.0...v0.94.0) (2026-10-19) ### Features * send one end event after the session/load history replay for AIR ([#1361](https://github.com/agentclientprotocol/claude-agent-acp/issues/1361)) ([v0.94.0 |
||
|
|
2dab922644 |
fix: update to Claude Code 2.1.303 (#1384)
Updates `@anthropic-ai/claude-agent-sdk` from 0.3.300 to **0.3.303**
(Claude Code **2.1.303**), along with dependabot's minor-group bumps
(`@anthropic-ai/sdk`, eslint, vitest and transitive packages). 0.3.304
is only on the `next` dist-tag, so it is not included.
## Fixes for changes in the new SDK
The Agent SDK turns on fork subagents (`subagent_type: "fork"`) by
default in SDK sessions (0.3.302). Before this, ACP sessions never got
them. Live streaming already handles forks. History replay did not:
- **Replay showed the parent's launching message inside the fork.** A
fork's transcript starts with a copy of the parent's launching assistant
message (with the same API message id and every `tool_use`), then a user
message with a `"Fork started — processing in background"` result per
`tool_use` and a `<fork-boilerplate>…Your directive: <prompt>` text. On
`session/load`, the parent's text, tool calls and those results appeared
in the fork, and the fork's prompt showed the full boilerplate.
Replaying the copy also consumed the parent's tool-use cache entries, so
the parent's real Agent/parallel tool results were dropped. In the AIR
form, sibling forks were re-registered under the fork.
`readSubagentMessages` now drops everything before the directive message
and reduces it to the prompt (`stripForkPrefix`).
- **Lineage scan** (`subagentLineage`) ignores launch calls in a fork's
copied prefix, so it no longer attributes the parent's launches to the
fork.
- **Agent title** no longer shows `(high effort)` for a fork: a fork
ignores `effort` and runs at the parent's effort.
## Changelog review: nothing else to change
- **0.3.302: an omitted `permissionMode` now starts in `default` on
third-party providers.** The agent always passes `permissionMode`
explicitly, so this does not apply.
- **0.3.301: a full-disk login refresh now reports `invalid_request`
instead of `authentication_failed`/401.** This now maps to `bad_request`
rather than `auth_required`. That's the right outcome, since `/login`
cannot fix a full disk. Its text has no "Please run /login", so the
synthetic-login check does not catch it.
- **0.3.301: `setModel()` no longer fails at the usage limit.** This is
a free fix for model switches.
- **0.3.302: structured output refusals are now error results.** These
are covered by the existing result handling.
- **New APIs not adopted:**
- Per-tool `permission_policy` on sdk MCP servers: we register none.
- `reloadPlugins({ addPlugins })`: there's no plugin reload surface.
- `BUILTIN_TOOL_NAMES`/`LEGACY_TOOL_NAME_ALIASES`: `@alpha`, and nothing
here needs them.
- MCP `_meta.ui`: unused.
Verified: `npm run build`, `npm run check`, `npm run test:run` (3205
passed). New tests cover the fork replay trace (RFD form),
`stripForkPrefix`, the lineage scan with a fork transcript (it fails
without the fix), and the fork title.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
v0.93.1-preview.6
|
||
|
|
ff01e06579 |
fix: update to ACP SDK 2.0.0 (#1386)
Updates `@agentclientprotocol/sdk` from 1.9.0 to 2.0.0. SDK 2.0.0 makes the default entrypoints stable-only and removes the legacy connection classes ([migration guide](https://github.com/agentclientprotocol/typescript-sdk/blob/main/MIGRATION_STABLE_UNSTABLE.md)). - Protocol imports move to `@agentclientprotocol/sdk/unstable` and `@agentclientprotocol/sdk/experimental/v2/unstable`. The agent uses unstable features (session forking, `systemPrompt`, turn usage, subagents, v2 draft extras), so this keeps the same protocol surface as 1.9.0. No wire changes. - The scenario schema validator loads `schema/v1/schema.unstable.json`, because the unversioned `schema/schema.json` export was removed. - `remote-control.test.ts` uses the repo's own `AcpClient` type instead of the removed `AgentSideConnection`. - Runtime code already used the `agent()` app API, so it needed no migration. Verified: `npm run build`, `npm run test:run` (3199 passed), `npm run check`, and a separate typecheck of `examples/`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>v0.93.1-preview.5 |
||
|
|
e155ecb419 |
test: expect the v2 state snapshot after the replay end event (#1385)
`main` has failed CI since #1361, on one test: `ACP v2 prompts > ends a resume with replay with one end event for a client with loadReplayEnd`. Two PRs that merged separately collided: - #1366 made a v2 resume send a `state_update` snapshot after the replay and before the response ("v2 requires a snapshot of the foreground state after the replay", in `src/v2/agent.ts`). - #1361 added the `loadReplayEnd` end event and this test, which expected the end event to be the last update of the response. The test predates the snapshot, so each PR passed CI on its own. The snapshot is required by the v2 spec, so the code is right and the test is stale. The test now expects the end event followed by the snapshot, and still checks that the end event is sent once. Only `src/tests/acp-v2.test.ts` changes. ## Testing - `npm run check`, `npm run build` and `npm run test:run` pass (3199 passed, 30 skipped). - The same test fails on `main` at `bcf74c2` without this change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>v0.93.1-preview.4 |
||
|
|
bcf74c2fe8 |
fix: match OpenRouter model ids in the gateway model listing (#1365)
The model picker was empty behind OpenRouter: `normalizeModelId` did not match `anthropic/claude-opus-5.5:batch[1m]` to `claude-opus-5-5`. It now strips a leading `anthropic/` and a `:variant` suffix, and maps `5.5` to `5-5`.v0.93.1-preview.3 |
||
|
|
1420994ced |
feat: send one end event after the session/load history replay for AIR (#1361)
## What
A client that declares the AIR capability `loadReplayEnd` gets one extra
`session/update` after the history replay of `session/load`:
```json
{
"sessionId": "<loaded session>",
"update": {
"sessionUpdate": "session_info_update",
"_meta": { "jetbrains": { "air": { "version": 1, "loadReplayEnd": true } } }
}
}
```
The client declares the capability in
`clientCapabilities._meta.jetbrains.air.capabilities`, as it declares
the other AIR extensions. The agent does not advertise it.
## Why
ACP has no signal that the history replay of `session/load` is over,
other than the response. This adapter replays the transcript while the
Claude Code CLI starts, and the CLI start takes most of the load time.
So the history is whole long before the response. With this event, the
client can show the history at once.
The protocol side is the RFD
https://github.com/agentclientprotocol/agent-client-protocol/pull/2407.
## Design change
The first version of this PR marked each replayed notification with an
index (`loadSessionReplayIndex`) and the last one with
`loadSessionEndOfRecordedHistory`. To know the last one, it held the
latest update and queued every other outbound message of the session
behind it. That design is replaced with a capability-gated single end
event. The replay sends as before. Only one event is added at its end.
There are no indexes, no held update and no queue.
## When the event is sent
- After every replayed update, the subagent history that the parent
replay sends to the child sessions included, and before the
`session/load` response.
- Also for an empty history.
- For `session/load` of a session that starts while the replay runs, for
`session/load` of a session that is already live, and for the ACP v2
`session/resume` with `replayFrom: {type: "start"}`.
It is not sent:
- When the replay stops (the CLI start failed during the replay) or
fails.
- For a live update.
- To a client without the capability.
The update has no `title` and no `updatedAt`. A `null` there would clear
the value. The load can still fail after the event, for example when the
CLI start fails after the replay. The event means that the recorded
history arrived whole.
## Client status
JetBrains Air shows the history before the load answer only after the
Kotlin SDK delivers the updates of a loading session before the answer.
An SDK PR for this is in progress.
## Tests
- `create-session-options.test.ts`: the event comes once after the
replay and before the response; before the CLI start finishes; for an
empty history; for an already live session; not when the CLI start fails
part way; not when the replay fails; not without the capability. The
event has no `title` and no `updatedAt` key.
- `acp-agent.test.ts`: the subagent history from its own transcript file
comes before the event.
- `acp-v2.test.ts`: v2 `session/resume` with a replay ends with the
event before the response; a resume without a replay sends none.
- `docs/air-extensions.md`: a new "Load replay end" section, and the
capability and key rows.
v0.93.1-preview.2
|
||
|
|
424db87535 |
fix: resolve a skill's SKILL.md by its declared name, not by its directory name (#1372)
## Problem Claude Code advertises a skill by the `name:` of its `SKILL.md` frontmatter, which does not have to match the name of the directory — or of the symlink — that holds the file. `resolveSkillPath` joined the advertised name into each container path and only checked whether the result exists, so every such skill resolved to nothing. For an AIR client the consequence is visible: `getAvailableSlashCommands` attaches `_meta.jetbrains.air.kind = "skill"` and the `SKILL.md` path only when the path resolves, so the skill reached the client as a plain command and disappeared from the skill completion, while `/name` kept working. Symlinked skills hit this most often, because the link name is usually chosen independently of the skill's declared name. Reproduced with a project whose `.claude/skills/mismatch/SKILL.md` declares `name: frontname`, and with `.claude/skills/aliased2 -> …/target2` declaring `name: front2`: both were advertised without any `_meta`. The same happens in a large repo where `.claude/skills/rubymine` declares `name: rubymine-workflows`. ## Fix The existing join-by-name probe stays the first attempt, so nothing changes for the common layout. When it misses, the same containers are scanned, in the same order, for a `SKILL.md` whose frontmatter declares the requested name: - `statSync`/`existsSync` follow links, so a skill behind a symlinked directory is scanned like any other; - each container is listed per lookup, and a `SKILL.md` whose modification time has not changed keeps the name of the previous scan, so an unchanged file is parsed once; - only the frontmatter block of at most 8 KB is parsed, so a `name:` in the body is not a name; - a directory that is itself named like the skill still wins over another skill that merely declares the name. ## Tests Eight cases in `src/tests/tools.test.ts` cover a directory/declared-name mismatch, a symlink with a differing link name in both `.claude/skills` and `.agents/skills`, the precedence of the directory that carries the name, scoped and plugin spellings resolved by declared name, a file without frontmatter, and a declared name that changes after the first resolution. --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Happy <yesreply@happy.engineering>v0.93.1-preview.1 |
||
|
|
dbf386d8d3 |
chore(main): release 0.93.0 (#1369)
🤖 I have created a release *beep* *boop* --- ## [0.93.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.92.0...v0.93.0) (2026-10-16) ### Features * add /remote-control command to start Remote Control for a session ([#1197](https://github.com/agentclientprotocol/claude-agent-acp/issues/1197)) ([v0.93.0 |
||
|
|
db9f50f021 |
feat: add /remote-control command to start Remote Control for a session (#1197)
Adds a `/remote-control [name]` command (alias `/rc`) so a session hosted over ACP can be continued from claude.ai/code or the Claude mobile app. Addresses #459. The CLI's own `/remote-control` is terminal-bound and isn't advertised to SDK hosts, and `remoteControlAtStartup` only applies to interactive sessions. The SDK does expose a runtime `enableRemoteControl(enabled, name?)` on the query object, which returns the session URL. See anthropics/claude-agent-sdk-typescript#460. - The adapter handles the command itself in `prompt()`, calls `enableRemoteControl(true, name)`, and replies with the session URL as an agent message. The turn ends without going to the model. - Failures (for example Remote Control disabled by policy) are reported to the user in the same way. - The command is added to `available_commands_update` only when the query exposes the method. - `enableRemoteControl` is not in the published `Query` types yet, so it is typed locally the same way as `mcpAuthenticate`. ## Testing - `npm run check`, `npm run build` and `npm run test:run` pass, including new tests in `src/tests/remote-control.test.ts`. - Ran the built agent through `examples/simple-client.ts` against a real Claude Max session: the command was advertised, `/remote-control` returned a working `claude.ai/code` URL, and prompts sent from the Claude iOS app ran in the session and were answered. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Noah Eisenbruch <noaheisenbruch@gmail.com> Co-authored-by: Andrey Bragin <andrey.bragin@jetbrains.com>v0.92.1-preview.2 |
||
|
|
f5967fd6b4 |
fix: hold a fork back from the session list until its session/fork response is out (#1368)
## Problem `session/fork` copies the transcript and then starts the forked session, which takes several seconds. Meanwhile the list subscription sees the new transcript and sends `_session/list/changes` with the fork row before the fork response. The client meets an unknown session and lists it as a session of its own. JetBrains Air then refuses the fork (IJAI-4105). ## Fix - `ForkHolds` (in `session-index/list-subscriptions.ts`, owned by the session index service) holds the row of a fork until the `session/fork` response is out: - by `forkedFrom`, for an unsent row, while the transcript is copied; - by the fork id after the copy. This covers a fork whose first record is over 64 KB, because the scan reads `forkedFrom` from the head only. - `session/list` leaves a held fork out too. - The v1 and v2 fork handlers run in `forking(...)`. A `setTimeout(0)` releases the rows after the SDK has written the response. - The holds live in the service, so a subscription that starts during a fork holds that fork too. ## Tests `holds a fork back until the response of its session/fork is out` (session-index.test.ts). It fails without the fix.v0.92.1-preview.1 |
||
|
|
af3d7f9335 |
chore(main): release 0.92.0 (#1358)
🤖 I have created a release *beep* *boop* --- ## [0.92.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.91.0...v0.92.0) (2026-10-16) ### Features * add the MCP server and tool to AIR tool calls ([#1359](https://github.com/agentclientprotocol/claude-agent-acp/issues/1359)) ([v0.92.0 |
||
|
|
68bf74f58e |
feat: update ACP SDK to 1.9.0 and adopt the v2 spec clarifications (#1366)
Updates `@agentclientprotocol/sdk` to 1.9.0 (schema v1.26.0, v2.0.0-alpha.9) and adapts the agent to the v2 spec clarifications in that release. ## Changes **Client-provided system prompt (unstable)** - Advertises `sessionCapabilities.systemPrompt` with `mode.override` on v1 and `session.systemPrompt` on v2. - Accepted on `session/new`, `load`, `resume`, and `fork`: - `append` adds the text after the Claude Code preset. - `override` replaces the preset with the text. Tools, MCP servers, permissions, and settings (including CLAUDE.md through `settingSources`) are unchanged. - An unsupported mode or non-text content fails with `invalid_params` before any session is created, reused, or forked. - Takes precedence over the `_meta.systemPrompt` extension. AIR custom instructions still follow the client text. - A different prompt on resume recreates a live session. Claude Code reuses its saved system prompt until the next compaction, which the RFD allows. **v2 session/resume state snapshot** - Sends the current foreground state (`idle`, `running`, or `requires_action`) after any replay and before the response, as the clarified spec requires. This applies even without a replay. **v2 replay of subagents** - No longer replays a child's past `running`/`idle` as current activity. Each replayed child gets one `state_update` with its last state after the replay. **v2 tool call `_meta` patches** - Reports carry only the top-level keys that changed. `claudeCode` is still sent whole, since clients don't merge nested objects. - A removed key is sent as `null`. - A tool call id that starts again removes the old keys. - A replayed tool call this connection has not reported sends `_meta: null` first, then the current keys. **Cancellation and close** - A `session/close` whose turns don't end within the teardown timeout settles them as cancelled. The client gets `idle` with `cancelled`, or `-32800` for a prompt never inserted, before the close response. - A prompt racing `session/close` ends as cancelled (`-32800` on v2) instead of `-32603`. On v1 it now returns `stopReason: cancelled`, matching a prompt queued just before the close. - A stream error after a cancel ends the work as `cancelled` instead of `error`. ## Not in this PR - After a URL-mode MCP elicitation is accepted, the session reports `running` right away. The spec asks for `requires_action` until the external interaction finishes, but a server may never send `elicitation_complete`, so this needs its own design. - `requires_action` is reported while one foreground subagent waits, even if other foreground work can continue. This is SHOULD-level in the spec. ## Testing - `npm run build`, `npm run check`, and `npm run test:run` pass. - Golden files changed because of the new capability and the resume snapshot. `compare.ts` allows the new capability against origin/main. - `session index cost > reads only the changed transcript for a subscription` failed intermittently locally (about 3 in 15 runs) on a loaded machine. This PR doesn't touch the session index, but watch it in CI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>v0.91.1-preview.5 |
||
|
|
26f144a2a5 |
fix: restore resumed subagents after an adapter restart (#1364)
After a restart the AIR subagent map is empty, so a SendMessage resume opened no generation and the child's messages stayed buffered under the old launch id. Restore the agent from the session history on a resume signal or on a message with an unseen parent_tool_use_id, with a generation id that can't collide with ids the client stored before.v0.91.1-preview.4 |
||
|
|
872e93f11f |
feat: add the MCP server and tool to AIR tool calls (#1359)
AIR gets `_meta.jetbrains.air.mcp = {server, tool}` on MCP tool calls,
so the MCP card does not depend on the title format.
The adapter splits the `mcp__<server>__<tool>` tool name the way Claude
Code does. `server` is the normalized name. Clients other than AIR get
nothing new.
Split from IJAI-872. The SDK does not expose MCP progress yet, so this
PR does not forward it.
v0.91.1-preview.3
|
||
|
|
78a29f58ba |
feat: publish the predicted follow-up prompt as _meta.promptSuggestions (#1356)
## What
The Claude Code CLI can predict the prompt the user is likely to send
next. This adapter asked for nothing and dropped the `prompt_suggestion`
message. It now asks for the prediction (`promptSuggestions: true`) and
forwards the one prediction of a turn.
ACP has no field for a composer prefill, so the value travels as
```json
{
"sessionUpdate": "session_info_update",
"_meta": { "promptSuggestions": { "suggestions": [{ "text": "Run the tests" }] } }
}
```
The key names no vendor, so another client can read it. AIR already
renders the first non-blank text as gray text in an empty composer,
where Tab fills the composer and sends nothing.
## Behavior
- `suggestions: []` clears the prediction the client holds: at the next
`session/prompt`, because that turn's prediction is spent, and on a
`conversation_reset`, because the transcript it was drawn from is gone.
- A prediction the next prompt outran — the user sent the next prompt
before the adapter read the prediction off the SDK stream — is dropped
instead of published mid-turn.
- A blank prediction never goes on the wire.
- The update is sent only to an AIR client, like the other extension
keys, so non-AIR traffic is unchanged (the key is added to
`AIR_ONLY_META_KEYS`; the `origin/main` scenario baselines stay
byte-identical).
- The CLI predicts nothing on the first turn, after an API error, in
plan mode, and at the usage limit, so a turn without a prediction is
normal. A caller can opt out with
`_meta.claudeCode.options.promptSuggestions: false`.
Documented in `docs/air-extensions.md`.
## Tests
`src/tests/prompt-suggestions.test.ts` covers the wire shape, the
publish after the turn's result, the clear at the next prompt, the
dropped stale prediction, the blank prediction, and a non-AIR client.
`src/tests/create-session-options.test.ts` covers the SDK option and the
caller opt-out.
`npm run check`, `npm run build` and the touched suites are green; the
full `test:run` has pre-existing flakiness on this machine
(session-index watchers, providers) unrelated to this change.
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
v0.91.1-preview.2
|
||
|
|
910d83ceea |
fix: include subagent file edits in the file-change report (#1357)
## Summary `agentFileChangeReport` took its path list only from Claude's file checkpoints (`rewindFiles` dry run). A checkpoint tracks only the files that the root agent edits with its own file tool. So a file that only a subagent edited never appeared in the report. Air then showed no "Since last turn" row for it, and Last Turn review could not include it. - Collect the `file_path` / `notebook_path` of successful `Edit`/`Write`/`MultiEdit`/`NotebookEdit` results from subagent messages in the stream. Store them on the active turn. This also covers background subagents, because the turn stays open until they finish. - Merge these paths with the checkpoint list. When the checkpoint preview times out or is unavailable, report the subagent paths instead of `unavailable`.v0.91.1-preview.1 |
||
|
|
c0af592d25 |
chore(main): release 0.91.0 (#1333)
🤖 I have created a release *beep* *boop* --- ## [0.91.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.90.0...v0.91.0) (2026-10-15) ### Features * keep background subagents running after a parent cancel, for clients that can cancel them ([#1348](https://github.com/agentclientprotocol/claude-agent-acp/issues/1348)) ([v0.91.0 |
||
|
|
761b4c8359 |
fix: update Claude Agent SDK to 0.3.300 (#1355)
Updates `@anthropic-ai/claude-agent-sdk` from 0.3.296 to 0.3.300 (parity
with Claude Code v2.1.300), and `@anthropic-ai/sdk` from 0.133.0 to
0.134.0.
No code changes needed. The new types are an optional `property_order`
on elicitation requests, `nextFireAt` on the CronCreate/CronList
outputs, and `owned` on Artifact outputs. We don't need any of them.
Fixes picked up that affect this agent:
- `importSessionToStore` (used for session forks) no longer skips
transcript rows containing U+2028/U+2029 on Node 24.
- Resuming an SDK session after a compaction no longer loses the recent
messages or the compaction summary in some cases.
- Background subagents in auto mode no longer send their final report
twice or merge it into another turn.
- Queued content-block prompts merged into one turn are now separated by
a line break.
- `ScheduleWakeup` now fires while a background task is running.
Behaviour change: `MAX_THINKING_TOKENS=0` (sent as `thinking: { type:
"disabled" }`) now runs adaptive thinking on models that can't turn
thinking off.
`npm run build`, `npm run check` and `npm run test:run` all pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
v0.90.1-preview.16
|
||
|
|
1a1f1a0e80 |
feat: mark session list rows of worktrees Claude Code made (agentWorktree) (#1352)
## What Marks `session/list` and `_session/list/changes` rows whose session runs in a linked Git worktree that Claude Code itself created (`claude --worktree`, the `EnterWorktree` tool), as `_meta.jetbrains.air.agentWorktree: true`. A new agent capability `sessionWorktrees`, advertised together with `sessionIndex`, tells the client that rows carry the field, so a missing field means "not made by the agent" rather than "unknown". ## Why With `includeWorktrees`, a client gets sessions from every linked worktree with nothing to tell them apart. A worktree Claude Code made for a session belongs to the project the user started it from; a worktree the user made by hand is a separate checkout. Only the agent knows which is which. ## How - The rule reads the transcript's `worktree-state` records: the latest one wins; the session counts only when `worktreeSession` is non-null and `enteredExisting` is not `true`. `environment.isWorktree` is not used, since plain git detection is also true for user-made worktrees. - The record is parsed in the existing head/tail transcript scan, so no extra file reads. The row is marked only when its `cwd` is in a linked worktree (`<common-dir>/worktrees/<name>`), never for the main checkout or a submodule. - The row's `cwd` stays the session's own directory. The field is `true` or absent. Proposed as `SessionInfo.agentWorktree` plus a `agentWorktrees` capability marker in the session list extensions RFD: agentclientprotocol/agent-client-protocol#2323. Docs: `docs/air-extensions.md`. ## Tests `src/tests/session-index.test.ts`: marked worktree, `enteredExisting`, no record (user worktree), a later `null` record, main checkout, `_session/list/changes` rows, capability negotiation, and scan windows. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>v0.90.1-preview.15 |
||
|
|
0c8de1380f |
fix: keep a live session that was never prompted when a resume or load changes its settings (#1353)
Found by running the ACP TCK's v2 suite against the adapter (ACP-ADDDIRS-202). The code is shared, so it also fixes v1's `session/load` and `session/resume`. ## The bug A `session/resume` or `session/load` can change a live session's settings: its `cwd`, additional directories, MCP servers, or the other settings in its fingerprint. When that happens, `getOrCreateSession` tears the live query down and starts a new one with `resume: sessionId`. Claude Code writes the conversation only at the first prompt, so for a session that was never prompted, the resume failed with `-32002` "Resource not found". The live session was already torn down by then, so the client lost a session it had a moment earlier. ## The fix The sign-out path already handled the same case: it tries `resume`, and on `resourceNotFound` it starts a fresh query under the same id with `reuseSessionId`. That logic is now one helper, `recreateLiveSession`, used by both: - **`getOrCreateSession`** uses it when the session was live. A session that was never prompted then starts a fresh query under its own id. It loses no history, because it had none, and it keeps its mode. - **A session that isn't loaded here** still resumes directly. If it has no stored conversation, that still fails with `-32002`, since the session really is unknown. - **`recreateSignedOutQuery`** keeps its own handling: if both attempts fail, it puts the old session object back. ## Checks - **ACP TCK v2** (`agentclientprotocol/acp-tck` at `fa00ccd`) against this build: ACP-ADDDIRS-202 now passes, where it failed before. ACP-ADDDIRS-201 and ACP-RESUME-201 to 205 still pass. - **Live v1 `session/load`**, Claude Code 2.1.296: create a session, then load it with a new additional directory before any prompt. - On `main`, the load failed with `-32002`. - With this fix, the load succeeds. A prompt then read a file that exists only in the new directory, and the transcript was saved under the same session id. - **Tests:** three new tests: - a live session that was never prompted starts a fresh query with its mode; - an unknown session still fails; - another failure of a live session's resume doesn't start a fresh query. Removing the fallback, applying it to every error, or applying it to sessions that aren't loaded each fails a test. The sign-out tests pass through the shared helper. - **Suite:** `npm run check`, build and all 3068 tests pass, with no golden changes. **Not covered:** a model chosen with `session/set_config_option` before the first prompt resets to the default when such a session is recreated, as it already did after a sign-out. The mode is kept.v0.90.1-preview.14 |
||
|
|
fadbe1881c |
fix: let session/delete succeed for a session without a transcript (#1351)
Found by running the ACP TCK's v2 suite against the adapter
(ACP-DELETE-201 and ACP-DELETE-203). The code is shared, so v1 clients
get the fix too.
## The bug
ACP says: "Deleting an already-deleted session, or a session that never
existed, **SHOULD** succeed silently." For a client that is neither AIR
nor a `sessionIndex` client, `session/delete` instead failed with
`-32603` "Internal error" in three cases:
- **A session this connection ran but never prompted.** Claude Code
writes the transcript at the first prompt. So the adapter closed the
live session, then the SDK delete threw "not found in any project
directory".
- **An unknown session.** The SDK delete throws the same "not found"
error.
- **An id that isn't a UUID.** The SDK delete throws "Invalid
sessionId".
## The fix
A new `deleteSessionIfStored` in `src/session-index/service.ts` handles
every other client's delete:
- An id that isn't a session id can't name a transcript, so it returns
`{}` without calling the SDK.
- The SDK's own "not found" means nothing is on disk, so that also
returns `{}`. It's recognised by the existing `isSdkNotFound`.
- Any other SDK failure, such as a permission error, still fails the
request.
The two AIR paths are unchanged, because `docs/air-extensions.md`
documents their errors: `-32002` for an unknown session on a
`sessionIndex` client, and the SDK's error for AIR without
`sessionIndex`. The doc now describes the plain client's behavior.
## Checks
- **ACP TCK v2** (`agentclientprotocol/acp-tck` at `fa00ccd`) against
this build: ACP-DELETE-201 and ACP-DELETE-203 now pass, where both
failed before. ACP-DELETE-202 is still skipped, because `session/list`
doesn't show a session that was never prompted.
- **Tests:** three new tests: a never-prompted live session, an unknown
session, and an id that isn't a UUID. Removing either rule, or
swallowing every error, fails a test. The existing delete tests now use
UUID ids, which is what real sessions have.
- **Suite:** `npm run check`, build and all 3068 tests pass, with no
golden changes.
v0.90.1-preview.13
|
||
|
|
ba3c7be05e |
feat: keep background subagents running after a parent cancel, for clients that can cancel them (#1348)
Stacked on #1347, which gives each running child the `cancel` capability. Merge #1347 first; this PR's base is its branch. ## What changes On a client that gets the subagents RFD form (v1 with `clientCapabilities.subagents` and not AIR, and v2), `session/cancel` on the parent now spares background subagents. They keep running, as acp#2308 prescribes ("a background child that the cancelled work did not wait on may continue"), and the client can stop each one with its child's `cancel` from #1347. Foreground subagents that the turn waits on still stop. Plain v1 clients and AIR are unchanged: the interrupt still stops their background subagents, since they have no way to stop one. ## How - **The declaration.** Claude Code's interrupt kills background subagents and workflows unless the query declares `perTaskStopAffordance`. The SDK describes it as "this consumer renders a per-task stop control", and an RFD client now has that control. The adapter declares the option for those clients. It comes before `_meta.claudeCode.options`, so a client can turn it off. - **Workflows.** The declaration also spares background workflows (`local_workflow`). A workflow's agents are not subagent tasks and get no child session, so a client can't stop a workflow unless it is AIR with `asyncTasks`. When a session declares the option and the client has no such control, `session/cancel` stops each live background workflow with `Query.stopTask` after the interrupt. Workflows therefore behave as they did before. - **Background commands** survive an interrupt with or without the declaration (checked live and in the CLI code), so nothing changes for them. Where I read Claude Code 2.1.296's CLI: without the declaration, the interrupt runs a kill of every running background `local_agent` and `local_workflow` task. With it, the interrupt kills only observer tasks. Adapter interrupts name no prompt, so this runs on every `session/cancel`, including one with no turn running. The declaration's only other effect is on requests with `interrupt_if_running`, which the adapter never sends. ## Checks **Live, Claude Code 2.1.296, through the built adapter.** Every update was checked against the schema, and no run had a violation or left a tool call open. Survival was checked by whether the background command's process was alive 2 s after the cancel. | Client | Background work at the cancel | Result | |---|---|---| | v1 RFD | subagent running a command | Prompt `cancelled` 9 ms after the cancel. The child kept running, completed its command and reported `idle end_turn`. Claude Code's follow-up cycle then streamed to the root. | | v1 RFD | subagent with an open permission request | The request stayed open through the cancel. The client allowed it 5 s later, and the child ran the command and ended `idle end_turn`. | | v1 RFD | workflow | The workflow and its command stopped, with no follow-up cycle. Same as the plain client. | | v1 plain | subagent | Unchanged: the interrupt killed the child, and its command's tool call ended `failed`. | | v1 plain | workflow | Unchanged: stopped, no follow-up. | | v2 | subagent | Root `idle cancelled` 11 ms after the cancel. The child kept running and later reported `idle end_turn`, and the root went `running`, then `idle end_turn`, for the follow-up. | | v2 | workflow | Stopped, no follow-up. | **Direct SDK probes** decided the design: - With the declaration, an interrupt spares a background workflow. - Without it, the interrupt kills the workflow. - A `stopTask` on a workflow, before or after the interrupt, stops it and its command, and Claude Code starts no follow-up cycle, the same as when the interrupt killed it. **Tests:** - Option tests: the declaration for each client form, a caller's override, and when a cancel stops workflows. - Cancel tests: only workflows are stopped, after the interrupt and only when the flag is set. - Removing any of the eight rules fails a test. - `npm run check`, build and all 3020 tests pass, with no golden changes. ## Not covered - A paused background workflow (one Claude Code restored on a resume) would also be stopped at a cancel, where the interrupt left it alone. I didn't check whether such a workflow reports `task_started`. - Standard ACP has no control to stop background work that is not a session, such as a workflow or a background command. Only AIR has one, through its async tasks extension.v0.90.1-preview.12 |
||
|
|
aeb69e7ed1 |
feat: let a client cancel one subagent with session/cancel on its child session (#1347)
A client on the subagents RFD can now stop one subagent without
cancelling the turn. Each running child is announced with `capabilities:
{ cancel: {} }`, and `session/cancel` with the child's session id stops
that child's current work.
## What Claude Code does
From direct SDK probes of `Query.stopTask(taskId)` and runs through the
adapter (Claude Code 2.1.293 and 2.1.296):
| Case | What Claude Code sends |
|---|---|
| Foreground subagent running a command | At once: `task_updated`
`killed` and `task_notification` `stopped` for the subagent, `stopped`
for its command's task, the command's interrupted result, then the Agent
call's interrupted result (`non_execution_kind: "interrupted"`). The
parent's turn goes on and ends normally. |
| Background subagent | The same events and result, then Claude Code's
follow-up cycle about the stop. |
| Subagent with an open permission request | The request is withdrawn
(its `AbortSignal` fires), and both calls get results. |
| Subagent waiting on its own foreground subagent | The grandchild is
stopped too, with its own `killed` and `stopped`, which is the cascade
the RFD requires. |
## The change
- **The grant:** `announceNativeSubagent` grants `cancel` to each live
RFD child. A replayed child gets no capabilities, since it no longer
runs (the RFD's freshness rule for reconnection). A routing-only
ancestor that a restore announces gets none either.
- **The cancel:** `cancel` checks first whether the session id is a
child of one of its sessions (`NativeSubagentRuntime.cancelChild`), and
if so calls `Query.stopTask` with the child's agent id. It sends no
interrupt, so the parent's work goes on. A stop that Claude Code refuses
is logged.
- **The report comes from the SDK's events,** `killed` or `stopped`,
which `finishTask` already turns into `idle` with `cancelled`. Accepting
the stop is no evidence, as the RFD says, and a cancel that races with a
child's end does not rewrite its outcome.
- **Requests:** a request that the stop withdraws reports no `running`
before `idle cancelled`. `awaitingUser` now counts cancels of the child
and of its ancestors, as it counted the root's (#1337). An ancestor's
cancel stops the foreground subagents under it.
- **Unchanged:** AIR keeps its draft and grants nothing, and a cancel of
a root session works as before.
## Evidence
**Live, through the adapter** (Claude Code 2.1.296, Sonnet), every
update checked against the schema, with no violations:
| Run | Child | Parent |
|---|---|---|
| v1 RFD, foreground child running a command | `running`,
`requires_action`, `running`, `idle cancelled` 9 ms after the cancel;
its command `failed` | Agent call `failed`; turn ends `end_turn` |
| v1 RFD, background child while v1 holds the turn | `idle cancelled`;
its command `failed` | the held turn settles with `end_turn` |
| v1 RFD, child with its permission request open | `requires_action`,
then `idle cancelled`, with no `running` between | turn ends `end_turn`
|
| v2, foreground child | `idle cancelled` on the root's stream and its
own | Agent call `cancelled`; root `idle end_turn` |
No tool call was left open in any run.
**Tests:**
- `native-subagents.test.ts`:
- `cancelChild` stops the child's task and reports nothing itself, and
the SDK's `killed` reports `idle cancelled`;
- no stop for a session that is no child, or for AIR;
- no `running` for a request that the child's own cancel withdraws, or
that its parent's cancel withdraws.
- `acp-agent.test.ts`: `session/cancel` on a child calls `stopTask`, not
`interrupt`, and the turn ends `end_turn`; a refused stop is logged;
nothing happens for an unknown session id or an AIR child.
- `acp-v2.test.ts`: the same over v2, with the child's `idle cancelled`
on both streams.
- **Golden files:** the six v2 subagent golden files gain `capabilities:
{ cancel: {} }` on each announcement, and nothing else. No AIR golden
file changes.
- **Mutation checks:** removing each of these fails at least one test:
- the grant;
- the routing to the child;
- the child's cancel count;
- the ancestors in the count;
- the AIR exclusion;
- the error handling.
So does reporting `idle cancelled` as soon as the stop is accepted.
`npm run check`, build and the full suite (3014 tests) pass.
## Not changed, and a decision
- **A parent cancel still stops background subagents.** Claude Code does
that because the adapter does not declare the SDK's
`perTaskStopAffordance` option. Declared, an interrupt spares background
agents and background shell commands (checked live: both ran to
completion after the interrupt), as
agentclientprotocol/agent-client-protocol#2308 prefers. Background shell
commands would then outlive a cancel too, and ACP has no control to stop
them, so declaring it is left for a separate decision.
- **Order:** Claude Code sends the stopped child's command result right
after its `killed` event, so the client sees `idle cancelled` a few
milliseconds before that call ends. The RFD has the client mark the
child's unfinished calls cancelled at that point and still accept later
updates.
- **On v2,** the stopped child's command ends `failed`, not `cancelled`:
Claude Code's result for a subagent's command carries no non-execution
kind.
|
||
|
|
e8eb9d340f |
feat: narrow models to what the providers/set gateway lists (#1340)
### Summary Addresses [IJAI-1783](https://youtrack.jetbrains.com/issue/IJAI-1783). An organization can restrict the models its JetBrains gateway serves, but a session routed through `providers/set` offered the whole Claude Code catalogue, so the picker showed models the gateway then refused. `providers/set` now lists the gateway's models before it responds, and the model picker offers only the SDK models the gateway lists. - `anthropic`: `GET {baseUrl}/v1/models?limit=1000`, paginated by `has_more`/`last_id`, with the provider headers and `anthropic-version`. - `bedrock`: `GET {baseUrl}/foundation-models` (`ListFoundationModels`, `modelSummaries[].modelId`), the listing the JetBrains gateway answers on its Bedrock route. Only `anthropic.` ids count. The cross-region prefix `global` when listed, else that of the first listed id, becomes the session's `ANTHROPIC_BEDROCK_REGION_PREFIX`, so the CLI calls the models under the prefix the gateway serves; ids of other prefixes, and SDK rows resolved to another prefix, are dropped. - Rows match by `resolvedModel` (or `value`) without dates, `-vN:M` suffixes, `[1m]` hints and provider prefixes; the narrowing runs after the `availableModels` allowlist. When the gateway does not list the model a session runs (the dropped default, or a resumed or recreated session's last model), the session switches to the first listed one with `setModel`, so the CLI does not keep running an unlisted model. - The listing runs in the provider-update queue, so `providers/set` and `providers/disable` apply in the order they arrive; logout also clears a `providers/set` that is still listing. - A 4xx answer means the gateway has no listing: `providers/set` succeeds and nothing is narrowed. Any other failed listing (network error, 5xx, unexpected body, 10 s timeout) fails `providers/set` with `internal_error`, and the previous provider stays in effect. An invalid header or a base URL with credentials fails without repeating the value. A listing that matches no SDK model leaves the session with no model to offer: there is no model config option. - `vertex`, gateways set through `authenticate`, and native routing are not listed. Setting the same gateway again reuses its listing; `providers/disable` and logout drop it. - `docs/model-configuration.md` describes the narrowing. ### Not covered Background haiku requests, subagents, and a `/model` typed into the prompt choose their own model and are not checked against the listing. ### Verification ```bash npm run check npm run build npm run test:run -- --no-file-parallelism # 2964 passed, 5 failed, 30 skipped; all 5 in session-index.test.ts, flaky on main too ``` Tests stub `fetch` (`src/tests/setup-no-network.ts` answers an empty listing by default) and cover the listing requests and pagination, the Bedrock region prefix and its env, region-aware matching, the dropped default, the failing `providers/set`, and the empty picker. The JetBrains Bedrock gateway's `/foundation-models` answers `global.anthropic.claude-{haiku,opus,sonnet}-5-5` (seen through Junie, which lists the same route); this adapter has not yet been run end to end against it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>v0.90.1-preview.11 |
||
|
|
d528647162 |
fix: report no running for the session's own request that a cancel withdraws, on ACP v2 (#1346)
## Problem On ACP v2 the adapter reports a session's foreground work as `state_update`s: `running`, then `requires_action` while a permission request or question that blocks the work is open, then `running` again, and finally `idle` with a stop reason. A request that ends because `session/cancel` or `session/close` stopped the work is not an answer, but it still reported `running`. #1337 fixed that for a subagent's child session and listed the session's own request under "Not covered". That request did not change, and since #1338 neither did its AskUserQuestion, which now ends at the abort too. On `main`, with Claude Code 2.1.296, v2, a Bash permission request that the client holds open, and then a cancel or a close: | What stops the work | States the session reported | | --- | --- | | `session/cancel` | `running`, `requires_action`, **`running`**, `idle cancelled` | | `session/close` | `running`, `requires_action`, **`running`**, `idle cancelled`, all before the close response | Claude Code's interrupt withdraws the request, so the SDK aborts the signal of the `canUseTool` call, the request's `finally` calls `syncAwaitingUser`, and with no blocking request left it reports `running`. The result that ends the work comes 2 to 9 ms later in these runs, so the client shows work that is being stopped as working. ## Cause `syncAwaitingUser` reported `running` whenever the last blocking request ended, whatever ended it. A request ends when the user answers it or when Claude Code withdraws it, and a withdrawal alone does not say whether the work was stopped. A `PermissionRequest` hook that answers also makes Claude Code withdraw the request, and the work goes on (9 s more in a live run), as #1337 found. So "a withdrawn request reports no `running`" is wrong. The rule that fits is #1337's: no `running` when the request was withdrawn after a cancel that came while it was open. ## Change - `cancelTurns` (called for `session/cancel`, and by the teardown of `session/close`) marks each request that is open, in `Session.openUserInputs`. - `withPendingUserInput` takes the request's `signal`. When the request ends with its signal aborted and the mark set, `syncAwaitingUser` stops waiting but reports no `running`. The four callers pass their signal: permission requests, AskUserQuestion, MCP elicitation, and the refusal fallback prompt. (#1337 counts the parent's cancels and compares the count when the request ends. Here each open request is already an entry of `openUserInputs`, so the cancel marks it. Same rule.) - Everything else is as before: - an answered request reports `running`, also after the cancel; - a withdrawal without a cancel reports `running`; - with two requests open, only the last one to end reports, by how that one ended; - a request that opens after the cancel is judged on its own, so a hook that withdraws it still reports `running`. - A foreground subagent's request blocks the turn, so it behaves like the turn's own. A background subagent's request does not block it (`blocksForeground`), so the root's states do not change for it. #1337's child states are unchanged. - `ForegroundEvents.running` and `v2Foreground` say when `running` is not reported. v1 reports no session state, so nothing changes there. AIR is unchanged: the scenario golden files do not change. ## Evidence Live runs against Claude Code 2.1.296 (Sonnet), driving `dist/index.js` over stdio as a v2 client (`CLAUDE_AGENT_ACP_EXPERIMENTAL_V2=1`) that holds the request and ignores `$/cancel_request`, with the session's cwd in default permission mode. Every update and request validated against the SDK's v2 JSON schema: 0 violations in all runs. The root's states: | Run | `main` | This PR | | --- | --- | --- | | `session/cancel`, Bash permission request | `running`, `requires_action`, `running`, `idle cancelled` | `running`, `requires_action`, `idle cancelled` | | `session/cancel`, AskUserQuestion | `running`, `requires_action`, `running`, `idle cancelled` | `running`, `requires_action`, `idle cancelled` | | `session/close` | `running`, `requires_action`, `running`, `idle cancelled` | `running`, `requires_action`, `idle cancelled` | | `session/cancel`, request of a foreground subagent (#1337's run) | root as above; child `running`, `requires_action`, `idle cancelled` | root `running`, `requires_action`, `idle cancelled`; child the same | | `PermissionRequest` hook allows after 3 s, no cancel | `running`, `requires_action`, `running`, `idle end_turn` | the same | | the client answers after 2 s | `running`, `requires_action`, `running`, `idle end_turn` | the same | | `kill -9` of Claude Code, no cancel | `running`, `requires_action`, `running`, `idle error` | the same (see below) | The hook run is why a withdrawal alone cannot mean "stopped": Claude Code withdrew the request 3 s in, and the command then ran 9 s more. A v1 run of the cancel (no state updates there) is identical on both builds. Tests, in `acp-v2.test.ts`, through the v2 client app, which validates every update: - For each of the four kinds of request (permission request, AskUserQuestion, an MCP server's elicitation, the refusal fallback prompt): a `session/cancel` withdraws it, a `session/close` withdraws it (the harness gets a `close` hook, as the SDK's cleanup of a closed query aborts open requests), and Claude Code withdraws it without a cancel. - The user answers after the cancel; a request that opens after the cancel and is then withdrawn; two requests open together (both withdrawn by the cancel, and one withdrawn and the other answered, in each order); a request of a cycle that Claude Code started. - Subagents: a foreground subagent's request also holds the root (an assertion added to #1337's test); a background subagent's request changes nothing of the root's states, alone and next to the turn's own request. 13 of them fail on `main` (12 new tests and the extended one); the other 8 new tests pass there and guard what must not change. Removal checks, each run against `acp-v2.test.ts`: | Removed or changed | Tests that fail | | --- | --- | | no rule: the end always reports `running` | 13 | | the cancel marks no request | 13 | | a mark is enough, the signal is not required | 2 (answered after the cancel) | | the simple rule "any withdrawal means stopped" | 5 (the hook cases, and the request after the cancel) | | `session.cancelled` in place of the mark of a cancel that came while the request was open | 1 (the request after the cancel) | | sticky: after one request was withdrawn by a cancel, no later request reports `running` | 1 (answers the one the cancel did not withdraw) | | permission requests do not pass their signal | 6 | | AskUserQuestion does not pass its signal | 3 | | MCP elicitation does not pass its signal | 2 | | the refusal fallback prompt does not pass its signal | 2 | `npm run check`, `npm run build` and `npm run test:run` pass (2999 tests passed, 30 skipped; the scenario golden files do not change). ## Not covered - **A stream end without a cancel.** If Claude Code exits by itself, the SDK's cleanup aborts every request before the consumer reads the end of the stream, and nothing at that moment tells it from a hook's withdrawal. Live (`kill -9` of the CLI, table above): `running` 2 ms before the `idle`, and the session is dead right after. Left as it is, for the reasons #1337 gives for children. - The rule assumes, like #1337's, that the work reports its end right after such a withdrawal. A turn always does (the force-cancel backstop ends a wedged one); a cycle with no active turn has no such backstop. - Open PRs #1344 and #1345 also change `src/acp-agent.ts`; this branch merges cleanly with each (checked with `git merge-tree`).v0.90.1-preview.10 |
||
|
|
594aa8c955 |
fix: close the tool calls that a cycle Claude Code started abandoned, at its end (#1344)
A model cycle that Claude Code starts on its own now closes, at its end, the tool calls that Claude abandoned before they ran, as a turn's end already does. ## The bug Claude can start a tool use and never finish the message that holds it, for example when the stream is cut off. The tool never runs, and no result comes. At a turn's end, `settleActive` closes such a call (`closeUnfinished`: "Claude stopped this tool call before it ran.", `failed` on v1, `cancelled` on v2). A cycle that Claude Code started on its own, such as its follow-up after a background command finished, has no turn. So nothing closed its abandoned calls, and the client kept them open. ## The fix `closeCancelledCycleToolCalls` becomes `closeCycleToolCalls` and runs at every end of a cycle: its result, or an idle that no result precedes. - **After a cancel:** unchanged (#1339, #1342). - **At any other end:** every call that ran already has its result. What is open is a call of the main conversation that was never dispatched, which is closed with the same report a turn's end sends. A subagent's calls are not the cycle's: a background subagent runs on. On v2 the report comes before the cycle's `idle`. ## Evidence **Tests:** - v1, a cycle that runs a command, starts a background subagent that is streaming a tool use as the cycle ends, and abandons a tool use: only the abandoned call is closed, once. The command keeps its one result, and the subagent's call stays open. - v1, a cycle that ends at an idle with no result (the stream dropped mid-cycle): its abandoned call is closed there. - v2: the abandoned call goes `pending`, then `cancelled`, before the cycle's `idle end_turn`. - **Mutation checks:** each of these fails at least one test: - no closing at a normal end; - closing a subagent's calls; - closing dispatched calls (an existing test catches it); - closing only at the result, not at the idle. `npm run check`, build and the full suite (2982 tests) pass, with no golden changes. **Not checked live:** Claude abandons a tool use only when something cuts its message off, such as a steer or a dropped stream, which a live run cannot set up reliably. |
||
|
|
bcc024d2c9 |
fix: deliver the output of a background subagent that a cancel did not stop (#1345)
After a cancel, the output of a background subagent that the cancel did not stop now reaches the client: its tool results, its new tool calls, and its permission requests. ## The bug The adapter has one rule for work that a cancel does not stop, as agentclientprotocol/agent-client-protocol#2308 asks: a background subagent that still runs (`runsInBackground`). The closing at the cancel leaves that subagent's tool calls open for their results (`closedAtCancel`). But the window after the cancel dropped the same subagent's output, results included: - **A subagent that completes after the cancel** kept its calls open. Its commands' results came after the cancel was reported and were dropped, and since it was not stopped, nothing closed them. - **A subagent that runs on** after the cancel showed nothing more: new tool calls were dropped, and its permission requests were denied as the stopped work's (#1334). Claude Code currently stops background subagents at an interrupt, as live runs show, so today this applies only to the output that comes before the SDK reports a subagent's end. It decides the rest once Claude Code lets them run. ## The fix `isRunningBackgroundSubagentOutput` uses the closing's predicate for the three places the window applies: - the drop of streamed output; - the record that `canUseTool` and the v2 preview hook read (#1334); - the guard of the user/assistant messages. A subagent's output after the SDK reported its end is still dropped, and #1336 closes its calls. ## Evidence **Tests** (`src/tests/incomplete-tools.test.ts`): - **A command's own result,** for a subagent that completes after the cancel, reaches the client, for a held turn and with no turn. This replaces "leaves the command of a subagent that completed after the cancel as it is", whose fixture sent no result at all. - **New work after the cancel was reported:** a command's streamed start, complete message and result reach the client. - **Permission requests after the cancel:** they are allowed, for a tool use that streamed first and for one that only its complete message holds. - **Unchanged:** a stopped subagent's late result is still dropped, and its call closed. - **Mutation checks:** removing each of the three uses, or widening the predicate to every subagent, fails at least one test. **Live, Claude Code 2.1.296,** a v1 cancel of a turn held for a background subagent that runs a command, on a plain client and an RFD client: Claude Code stopped the subagent, and its command was closed after the cancelled response, as on `main`. No schema violations. A subagent that survives a cancel cannot be set up live today. `npm run check`, build and the full suite (2983 tests) pass, with no golden changes. ## Not covered - **A foreground subagent nested in a background one:** its own Agent call is not backgrounded, so the closing closes its calls at the cancel, and the window drops its output, both as before. The two stay consistent.v0.90.1-preview.9 |
||
|
|
b4788d61ea |
fix: end the tool calls of a cycle that Claude Code started and a cancel stopped, on v1 (#1342)
On v1, when a cancel stops a model cycle that Claude Code started on its own, the cycle's open tool calls now end. Before, they stayed `pending` for good. ## The bug Example, from a live run (Claude Code 2.1.296, a v1 client): 1. A prompt starts a background command, and its turn ends. 2. The command finishes, and Claude Code starts a cycle on its own: it launches a foreground subagent, which runs a command. 3. The client sends `session/cancel` while no prompt runs. Claude Code stops both and sends the Agent call's interrupted result (`user-rejected`). The client never got it, and the Agent call stayed `pending`. Before #1336, the subagent's command stayed `pending` too. **Why:** the window after a cancel drops output once the client has been told that the stopped work ended (#1313). On v1 that report is the prompt response, read as "no active turn". A cycle never has a turn, so from the cancel on, everything it sent counted as late output, including Claude Code's results for its own tool calls. v2 tracks a cycle as foreground work and closes its calls before `idle cancelled` (#1339). v1 tracked nothing. ## The fix - **The agent tracks foreground work on every connection.** Only v2 reports it, through the `foreground` option; v1 gets events that do nothing (`UNREPORTED_FOREGROUND`). The rules for when a cycle starts and ends are the ones v2 already uses. - **On v1, a cycle's end is its result or its idle** (`workEndReported`). Until then, Claude Code's interrupted results reach the client. At that end, `closeCancelledCycleToolCalls` closes the calls that got none, as on v2. After it, the window drops what comes, as before. A v1 turn's rule is unchanged: no active turn. - **A held turn's follow-up shares the held turn's closing.** v1 holds a turn for its background subagents, and the held turn's follow-up is a cycle too, now on v1 as well. When a second cancel settled the held turn in `cancelTurns` while the cycle's closing was in flight, the two ran separate passes. The response then came before the cycle's first report, and on v2 a call could be closed twice. `closeCancelledCycleToolCalls` now uses the held turn's closing (`Turn.closingCancelledToolCalls`), which every settle of the turn waits for. The existing test "closes each tool call once, before the response, when a cancel settles a held turn that the consumer is closing" caught this. ## Evidence **Live, Claude Code 2.1.296, the scenario above**, every update checked against the schema (no violations): | Client | Agent call | Subagent's command | |---|---|---| | v1, before | `pending` | `failed` (closed by #1336) | | v1, this PR | `failed`, with Claude Code's own `user-rejected` result | `failed`, closed at the cycle's end | | v1 RFD client, this PR | the same | the same, in the child's session | | v2, this PR | `cancelled`, before `idle cancelled` | `cancelled`, before `idle cancelled` (unchanged) | A v1 cancel with a queued prompt, followed by the next prompt, still left no tool call open, and the next prompt's Write asked for its permission and ran. **Tests** (`src/tests/incomplete-tools.test.ts`): - **#1339's test for v1 now asserts the fix:** a cycle's command gets Claude Code's interrupted result. - **New:** the live scenario, where the Agent call gets Claude Code's result and the subagent's command is closed; and a cycle that ends at an idle with no result, where its call is closed at the idle and Claude Code's later result is dropped. - **#1336's "no turn" fixture** launched the background subagents in a cycle it never ended before the cancel, so v1 now delivered Claude Code's real result there. The fixture now ends that cycle, which is the "cancel while nothing runs" case the tests mean. - **Mutation checks:** each of these fails at least one test: - the v1 rule without the cycle; - no cycles on v1; - no foreground tracking on v1; - a separate closing for a held turn's follow-up. `npm run check`, build and the full suite (2979 tests) pass, with no golden changes. ## Not covered - A cycle that ends normally with a `tool_use` that Claude abandoned keeps that call open. - A subagent that completes after a cancel keeps its calls open.v0.90.1-preview.8 |
||
|
|
c1e83b46b7 |
fix: stop waiting for a question the client leaves open after Claude Code cancels it (#1338)
## Problem
A permission request is abandoned when Claude Code withdraws it: its
`signal` is raced against the client's answer
(`requestPermissionFromClient`), because the SDK only sends
`$/cancel_request` and keeps waiting for the client's reply, which a
client that ignores the cancel never sends. An elicitation is not raced.
A question that the client leaves open after Claude Code cancels it
never settles, and its request stays open on our side:
- **The session** keeps the question in `openUserInputs`
(`withPendingUserInput`) for good. Steering stays non-interrupting
(`priority: later`) for the rest of the session, and on v2 the next turn
starts as `requires_action` though nothing is open.
- **A child** that asked keeps its `openRequests` up. When it is resumed
later, `resumeChild` reports `requires_action` instead of `running`.
Live, on `main` (Claude Code 2.1.296, v2): the main conversation asks
with AskUserQuestion, the client holds the elicitation and ignores
`$/cancel_request`, the user cancels, then sends a second prompt, "Reply
with just the word hi.":
| | States of the second turn |
| --- | --- |
| `main` | `running`, **`requires_action`**, `idle end_turn` (the answer
"hi" streams while it waits) |
| This PR | `running`, `idle end_turn` |
## Change
One helper, `elicitFromClient`, sends the elicitation and races it
against the callback's `signal`, as `requestPermissionFromClient` does.
The three places that send an elicitation for a callback of Claude Code
use it:
- AskUserQuestion (`handleAskUserQuestion`), the one that a child can
send;
- MCP elicitations (`onElicitation`, `handleMcpElicitation`);
- the refusal-fallback dialog (`onUserDialog`, `handleUserDialog`).
They had the same defect, each tracks its request with
`withPendingUserInput`, and their handlers already read an abort as a
cancel (`{ action: "cancel" }`, `{ behavior: "cancelled" }`, "Tool use
aborted"), so only the wait changes.
Nothing else in the flow needs the answer to arrive. After the
elicitation, a question's tool call ends with Claude Code's own
`tool_result`. A url-mode elicitation completes with the SDK's
`elicitation_complete`, not with the answer. A client that does reply to
the cancel still settles it first, to the same outcome as before.
AIR is unchanged, and the golden files do not change.
## Tests
All of them fail without the helper (checked by removing the race, and
then by reverting each of the three call sites alone):
- `acp-agent.test.ts`, "does not wait for … when its client ignores
cancellation": each of the three paths ends as aborted and leaves
nothing open.
- `acp-agent.test.ts`, "uses priority:'now' again once … is withdrawn":
the existing table of every request that waits for the user (permission,
AskUserQuestion, MCP elicitation, refusal dialog, and a child's
permission request and question), with a client that never settles. The
permission rows are controls that already passed. I moved the table into
a constant so both tests use it.
- `acp-agent.test.ts`: a child's question that Claude Code withdraws,
and a SendMessage resume after its end, reports `running`.
- `acp-v2.test.ts`: the live scenario above: the next turn does not
report `requires_action`.
Removal checks: no race in the helper (9 tests fail), the
AskUserQuestion site reverted (5), the MCP site (2), the dialog site
(2).
`npm run check`, `npm run build` and `npm run test:run` pass (2935
tests).
## Not covered
- **A child's question live.** Claude Code 2.1.296 does not give
subagents AskUserQuestion: a general-purpose subagent, and a custom
agent with `tools: AskUserQuestion`, both reported that they could not
ask. So the child's path is covered by the unit test only. The root's
question runs the same `handleAskUserQuestion`, which the live run above
covers.
- **The root session at a cancel.** Now that the question settles at the
abort, the root reports `requires_action`, `running`, `idle cancelled`
for it, as it already does for a permission request (live, v2). That
flicker comes from `syncAwaitingUser`, and is not changed here.
- A signal that already aborted when the elicitation is sent still sends
it, and cancels it at once. A permission request checks first. Nothing
depends on it.
- The URL elicitation of an MCP server's sign-in
(`authenticateMcpServer`) keeps its own race against the authentication,
and is not a tracked request.
Related, and independent: #1337 reports no `running` for a child's
request that a cancel withdraws. The two PRs touch the same call site
and merge cleanly in either order (checked with `git merge-tree`).
v0.90.1-preview.7
|
||
|
|
c7425e4e04 |
fix: report no running for a subagent's request that a cancel withdraws, for subagents RFD clients (#1337)
## Problem For a subagents RFD client, a subagent's child session reports `requires_action` while one of its permission or question requests is open, and `running` again once its last open request ends (#1322, `NativeSubagentRuntime.awaitingUser`). A request that ends because the work was **stopped** is not an answer, but it still reported `running`. On `main`, with Claude Code 2.1.296, a foreground child whose Bash permission request is open, and a cancel on the parent: | What stops the work | States the child reported | | --- | --- | | `session/cancel` (v1, and v2 on the child's own stream) | `running`, `requires_action`, **`running`**, `idle cancelled` | | `session/close` | `running`, `requires_action`, **`running`**, and then nothing: a closed session reports no `idle` | Claude Code's interrupt withdraws the request, so the SDK aborts the signal of the `canUseTool` call, the adapter sends `$/cancel_request`, and the request's `finally` reports `running`. The SDK's own report of the child's end (`task_updated killed`) comes a few ms later (3 to 4 ms in these runs), so the client shows a child that is being stopped as working. After a `session/close` nothing corrects it. ## Cause `awaitingUser` reported `running` whenever the last open request ended, whatever ended it. A request ends in two ways, the user answers it or Claude Code withdraws it, and a withdrawal alone does not say whether the work was stopped. Claude Code also withdraws a request when something else answers it, and then the child runs on. I ran one such case, a `PermissionRequest` hook that allows the command after 3 s: the child worked for 9 more seconds after the withdrawal, and `running` was right. (The CLI also re-checks a pending ask when its permission context changes, and withdraws it if that now allows the call. I did not manage to trigger that: `session/set_mode` to `bypassPermissions` left the request open in my run.) So "a withdrawn request reports no `running`" is wrong: that child would sit on `requires_action` for the rest of its run. Waiting for the child's next update instead does not help either: in that run the child's first update after the withdrawal came 8 s later, with the tool's result. ## Change A request reports no `running` when it was **withdrawn after a cancel that came while it was open**. Then Claude Code's interrupt withdrew it as it stopped the child, and the SDK's events report the child `idle`. - `NativeSubagentRuntime.parentCancelled` (called for `session/cancel`, and by the teardown of `session/close`) counts the cancels in the RFD form. It still reports nothing, so #2308 is unchanged: a child that keeps running keeps its requests and its state. - `awaitingUser` takes the request's `signal`, which the agent passes for permission requests and AskUserQuestion (`withChildAwaitingUser`). A request records the count when it opens. If its signal aborted and the count changed, its end reports no `running`. - Everything else is as before: a request that the user answers, also after the cancel, reports `running`; a withdrawal without a cancel reports `running`; two overlapping requests report once, when the last one ends, by how that one ended. - README: one clause in the work-state bullet of "Subagent sessions". AIR is unchanged: `awaitingUser` only runs in the RFD form, and the AIR golden files do not change. ## Evidence Live runs against Claude Code 2.1.296 (Sonnet), driving `dist/index.js` over stdio with a client that holds the child's permission request, validating every update and request against the SDK's JSON schema (0 violations in all runs): | Run | `main` | This PR | | --- | --- | --- | | v1, `session/cancel` | `running`, `requires_action`, `running`, `idle cancelled` | `running`, `requires_action`, `idle cancelled` | | v2, `session/cancel` (child's stream) | `running`, `requires_action`, `running`, `idle cancelled` | `running`, `requires_action`, `idle cancelled` | | v1, `session/close` | `running`, `requires_action`, `running` | `running`, `requires_action` | | v1, hook allows after 3 s, no cancel | `running`, `requires_action`, `running`, `idle end_turn` | the same | Tests (all fail without the rule, as checked by removing it): - `native-subagents.test.ts`: the cancel withdrawal (and the child's `idle cancelled` after it); a withdrawal without a cancel (the hook case, which a simpler "abort means stopped" rule fails); a request that outlives a cancel and is answered later; a cancel that came before the request opened; two overlapping requests, in each order. - `acp-agent.test.ts`: through `canUseTool`, with `agent.cancel` and `agent.closeSession` aborting the request as Claude Code does, and the hook case. - `acp-v2.test.ts`: the child's own `state_update` stream and the parent's copy. Removal checks, each run against the three test files: no rule (6 tests fail), the cancel not counted (6), the agent not passing the signal (3: agent cancel and close, v2), and the simple "any withdrawal" rule (3: the hook cases). `npm run check`, `npm run build` and `npm run test:run` pass (2935 tests; the scenario golden files do not change). ## Not covered - **A stream end without a cancel.** If Claude Code exits by itself, the SDK's cleanup aborts every request before the consumer reads the end of the stream, and nothing at that moment tells it from a hook's withdrawal. Live (`kill -9` of the CLI): `running`, then `idle`, 1 ms apart. The session is dead right after. A rule for it would be a timing one: report `running` for a withdrawal with no cancel one event-loop turn later, so that `finishAll` goes first. I tried it (`await new Promise((resolve) => setImmediate(resolve))` before the check in `awaitingUser`'s `finally`): the `kill -9` run then reports `running`, `requires_action`, `idle`, and the hook run still reports `running` 1 ms after the withdrawal. I did not include it. It only works when the end of the stream reaches `finishAll` in the same turn (a worker shutdown publishes its failure notice first, for one), and it hides an ordering assumption about the consumer. Your call. - **Claude Code stopping a child on its own** (the model's TaskStop) while its request is open: the same shape, no cancel. - **The root session on v2** has the same flicker for its own request: `requires_action`, `running`, `idle cancelled` (live, v2). It comes from `syncAwaitingUser`, not from this runtime, and is not changed here. - A background child that outlives a cancel and whose still-open request a hook then withdraws would report no `running`. Claude Code stops background children at an interrupt today, so it does not arise. Related, and independent: a child's AskUserQuestion is not abandoned on an abort when its client ignores `$/cancel_request` (#1338). The two PRs touch the same call site and merge cleanly in either order (checked with `git merge-tree`).v0.90.1-preview.6 |
||
|
|
1fd217f3e7 |
fix: close the tool calls of a cycle that Claude Code started and a cancel stopped (#1339)
## Problem On ACP v2, a model cycle that Claude Code starts on its own, such as its follow-up after a background command finished, is foreground work: `state_update` `running`, then `idle`. When a cancel stops such a cycle, the tool calls that it left open can stay `pending` after `idle` `cancelled`. Claude Code answers each tool of the main conversation with an interrupted result, and the adapter delivers it before the cycle's `idle`. It sends nothing for what a foreground subagent of the cycle ran, nor for a tool call that Claude abandoned before it ran. No turn settles at the cycle's end, so `closeCancelledToolCalls` (#1327, #1221) never runs for it. Example, live (Claude Code 2.1.296, Sonnet): a background `sleep 6` ends its turn. When it finishes, Claude Code's follow-up runs a foreground subagent, which runs a 60 s command, and the client cancels while it runs: ``` root state_update running <- Claude Code's own cycle child tool_call gKT4K4 (the subagent's command) pending ... session/cancel ... root tool_call_update PLh9rB (Agent call) cancelled <- Claude Code's result child state_update idle cancelled root state_update idle cancelled child gKT4K4: still pending, for good ``` ## Change - `closeCancelledCycleToolCalls` ends the open tool calls with the cancelled report before the cycle's end is reported, at the cycle's result and at the idle that ends it when no result comes. The calls are the ones the turn's closing takes: every open call except those of a background subagent that still runs. - It applies when a cancel is in effect (`session.cancelled`) and the cycle is the foreground work (`runsAutonomousCycle`). A turn that took the cycle over closes its own calls when it settles. - The loop of `closeCancelledToolCalls` is now `closeOpenToolCalls`, and its filter is named `closedAtCancel`. These two hunks are the same as in #1336, so that the PRs merge in any order. - The follow-up cycle of a turn that v1 holds for its background subagents is a cycle on v2, and its closing now comes before its `idle`. It came at the held turn's settle, after it. ## Evidence **Tests** (6 new): - `acp-v2.test.ts`: the command of the cycle's foreground subagent ends `cancelled` in the child's session, after the child's idle and before the cycle's, once; a tool call that Claude abandoned in the cycle ends before `idle` `cancelled`; so does one at the idle that ends a cycle without a result; the follow-up cycle of a held turn closes before `idle` `cancelled`, and a background subagent that keeps running keeps its command open; a cycle that the SDK aborted without a cancel is left to Claude Code's result, which still reaches the client. - `incomplete-tools.test.ts`: on v1, which reports no cycle, a cancelled cycle's tool call is not closed (see below). **Removal checks.** Each of these fails at least one test: no closing at the result (3), none at the idle (1), closing a cycle that no cancel stopped (1), without the cycle being the foreground work (2), closing a running background subagent's calls (1), closing after the cycle's end is reported (3), the turn's closing taking those calls too (5). **Live**, every update validated against the SDK's v2 JSON schema (0 violations): | cancel while Claude Code's cycle runs | `main` | this PR | | --- | --- | --- | | a foreground subagent's command | `pending` in the child's session, after the root's `idle cancelled` | `cancelled` in the child's session, after the child's idle and before the root's | | a command of the cycle itself | Claude Code's result, `cancelled`, before `idle` | the same, unchanged | `npm run check`, `npm run build` and `npm run test:run` (2930 tests) pass. No golden file changes. With #1335 and #1336 merged into it, in any of the six orders, the branches merge without conflicts and all 2948 tests pass. ## Not covered, and a decision for the maintainer - **v1.** Nothing tracks a cycle on v1, and with no turn active every result after the cancel is dropped, Claude Code's for the cycle's own commands too. In the same live run on v1 the Agent call and the subagent's command both stay `pending`. Closing at the cycle's cancelled result there would end both, with a made-up status for the Agent call too. I left it: it changes what v1 clients see for a cancel while idle, and one existing test (a held turn that the consumer closes while a cancel settles it inline) fails once the cycle check is removed, so it is not only a gate. It also touches the post-cancel window of #1061, which another change owns. - A cycle that ends without a cancel keeps an abandoned `tool_use` open. `settleActive` closes that for a turn, and no turn owns a cycle. - A session that ends or closes with a cycle running ends the cycle as cancelled without closing its calls.v0.90.1-preview.5 |
||
|
|
9a1b90fb9d |
fix: close the tool calls of a background subagent that the SDK reports stopped after a cancel (#1336)
## Problem After `session/cancel`, the tool call of a **background** subagent's command stays `pending` for good when no turn is left to settle. Example, live (Claude Code 2.1.296, Sonnet): a background subagent runs `python3 -c "import time; time.sleep(60)"`, and the client cancels while it runs. Either the session is idle, or v1 holds the prompt open for the subagent and the cancel settles it at once. Claude Code's interrupt stops the subagent: the SDK reports `task_updated` `killed` and `task_notification` `stopped`, and the RFD form reports the child `idle` `cancelled`. The command's call is never closed, in the root session or in the child's. ## Cause #1327 closes a cancelled turn's open tool calls when the turn settles (`closeCancelledToolCalls`), and leaves a background subagent's calls open, because the interrupt may not stop it. Here the turn is already settled (the cancel settled the held turn before the interrupt) or there is none, and the stop comes after that. Nothing closes the calls then. Claude Code does send the command's interrupted result. It comes after the stop reports, so it is output after the work's end, and the `session.cancelled` check of the user/assistant case drops it, as it drops everything after that. The raw stream, in the order Claude Code sends it: ``` task_updated agent patch killed task_notification agent stopped task_notification the command's task stopped user (ptu=agent) tool_result for the command, is_error <- dropped ``` ## Change - **A subagent's stop closes its open tool calls** (`closeStoppedSubagentToolCalls`), at `task_notification` `stopped` and at `task_updated` `killed`, after `finishTask` has reported the child idle. It closes the calls whose recorded parent is that subagent's Agent call (`Session.toolCallParents`) with the cancelled report that #1327 sends, naming the subagent so that every client form routes it like the call's other updates. - **Only where nothing else will end the calls**: a cancel is in effect (`session.cancelled`), and the client was told that the work ended (`workEndReported`: the prompt response on v1, `idle` on v2). While the work runs, a stopped subagent's calls get Claude Code's own result or `closeCancelledToolCalls` at the work's end, as before. - The loop of `closeCancelledToolCalls` is now `closeOpenToolCalls`, which takes the filter for the calls: the turn's closing, named `closedAtCancel`, skips a running background subagent's calls, and this closing takes one subagent's. These hunks are the same as in #1339, which closes the calls of a cycle that Claude Code started, so that the PRs merge in any order. - README: one sentence on when such a call ends. Nothing is closed twice: a closing forgets the call before it reports it, so a later closing, and the result that Claude Code sends after the stop, find nothing. The cancel settles a held turn and closes its calls before it interrupts Claude Code, so the stop reports always come after that closing. ## Per client form - **v1 without subagents:** the call goes to the root session, after the response when the cancel settled a held turn. - **RFD:** the call ends in the child's session right after the child's `idle` `cancelled`, as #1327 does at a turn's settle. - **AIR:** the cancel already ended every child, so the runtime drops the report (`ignoring late update for terminal subagent`). The call's state is released, which it was not before. - **v2:** as RFD. The root reports nothing, since it had no work. ## Evidence **Tests** (10 new): - `incomplete-tools.test.ts`: for a held turn and for no turn, the command ends once with the cancelled report, after the cancel's response, and the other subagent's command stays open; each of the two reports ends it alone; a subagent that completed after the cancel is left alone; one that stopped without a cancel, and one that stopped while the work still runs, get Claude Code's result, not a made-up report. - `acp-agent.test.ts`: the RFD form (the child's session, after its idle, after the response) and AIR (nothing reaches the ended child). - `acp-v2.test.ts`: the child's command ends `cancelled` in its session after its `idle cancelled`, and the root reports nothing more. **Removal checks.** Each of these fails at least one test: no closing at the notification (2), none at the patch (1), closing without a cancel (1), before the work's end is reported (2), at any status of the notification (1), of the patch (1), every subagent's calls (4), calls of another parent (7), the turn's closing no longer skipping a running background subagent (7), the report without the subagent's name (7), and the closing before the child reports idle (2). **Live**, 60 s command in a background subagent, cancel 4 s after its permission request was answered, every update validated against the SDK's JSON schema (0 violations): | client | cancel while | `main` | this PR | | --- | --- | --- | --- | | v1, no capabilities | the held turn waits for the subagent | `pending`, response `cancelled` | `failed` 4 ms after the response | | v1, no capabilities | no turn runs | `pending` | `failed` after the SDK's first stop report | | v1, RFD | no turn runs | `pending` in the child's session | `failed` in the child's session, after its `idle` `cancelled` | | v1, RFD | the held turn | not run on `main` | `failed` in the child's session, after the response | | v1, AIR | the held turn | not closed | report dropped by the runtime, state released | | v2 | root idle | `pending` in the child's session | `cancelled` in the child's session, after its `idle cancelled` | `npm run check`, `npm run build` and `npm run test:run` (2934 tests) pass. No golden file changes. With #1335 and #1339 merged into it, in any of the six orders, the branch merges without conflicts and all 2948 tests pass. ## For the maintainer - **Closing, not delivering.** Claude Code 2.1.296 sends the result, so delivering it after the work's end would also end the call, with Claude Code's text. I closed instead: 2.1.293 sent none for a subagent's call (#1327), the result comes after the response in the window #1313 closes, and closing works for both. The cost is the made-up text ("The turn was cancelled before this tool call returned a result.") in place of Claude Code's. ## Not covered - A background subagent that the interrupt does not stop keeps running, and its calls stay open. Its output after a cancel is dropped until the next prompt, as before. - A subagent that completes after a cancel: Claude Code's results for its calls were dropped, and the calls stay open. Nothing says they were cancelled, so none is reported. Delivering those results would end them. - A subagent that a SendMessage resumed: its calls carry the id of the call that launched it, while the registry holds the SendMessage id. `runsInBackground` has the same limit. - The calls of a cycle that Claude Code started on its own are #1339.v0.90.1-preview.4 |
||
|
|
764f339c3e |
fix: end the terminal of a command whose tool call is closed without a result (#1335)
## Problem A tool call that ends without a tool result is closed by a report that only sets its status (`AcpToolCallRenderer.unfinished`). That happens after `session/cancel`, for a subagent's command (Claude Code sends no result for it) or a call that Claude abandoned, and when a turn ends with a call that has no result (`closeUnfinished` in `settleActive`). A client that renders terminals (the `terminal_output` / `terminal_output_delta` extension, and the display terminals of ACP v2) holds a terminal for a Bash command, which its `tool_call` started with `_meta.terminal_info`. The closing report did not end it, so the call shows as ended next to a terminal that never exits. Example, live (Claude Code 2.1.296, Sonnet): a foreground Agent call whose subagent runs `python3 -c "import time; time.sleep(45)"`, and `session/cancel` while it runs. Claude Code answers the Agent call and sends nothing for the subagent's command, so the adapter closes that call itself: - v1 client with `_meta.terminal_output`: `tool_call_update` `failed`, with no `terminal_exit`. - v2 client: `terminal_update` with the command, then `tool_call_update` `cancelled`, and no `terminal_update` that carries an exit. ## Change - `closeToolCall` is the one function that every closing report goes through (the cancelled-turn closing and the end-of-turn closing today, and any closing added later). It now also ends the call's terminal, in the same update, as the result of a command does. It reads the tool from the tool use cache before it drops the entry. - The closings that #1336 and #1339 add go through `closeToolCall` too, so they end the terminal as well once merged. - `AcpToolCallRenderer.terminalExit` decides whether the call has a terminal. It uses the rule that `toolCall` uses to start one, a command on a client that renders terminals, which is now one private method (`hasTerminal`) that `toolCall`, `toolUseContent` and `terminalExit` share. - The exit code is unknown, since the command ended without a result: `exit_code: null`, as `endBackgroundTerminal` sends for a background command. AIR always gets a number and reads 1 for a command that did not succeed (#1252), so it gets 1. A v2 client gets `terminal_update` with an empty `exitStatus`. - `docs/air-extensions.md`: one line in the terminal conventions. ## Evidence **Tests** (8 new): - `incomplete-tools.test.ts`: the cancel's closing of a subagent's command ends its terminal, still names the subagent, and sends `exit_code: null`; AIR gets `exit_code: 1`; a client without terminals gets no exit; a subagent's `Read` gets none; a command that ran but got no result by the end of the turn gets the exit; so does one that Claude abandoned before it ran. - `tools.test.ts`: `terminalExit` for Bash, PowerShell, v2, AIR, a client without terminals, and tools that are not commands. - `acp-v2.test.ts`: the stopped subagent's terminal exits in the child's session, after the command's terminal started and before the root's `idle cancelled`, and the call ends `cancelled`. **Removal checks.** Each of these fails at least one test: no exit in `closeToolCall` (5), AIR gets null (2), an exit for a client without terminals (2), an exit for a tool that is not a command (3), another terminal id (5), the exit replacing the parent meta (1), the tool read after the cache entry is dropped (5). **Live**, each update validated against the SDK's JSON schema (v1 `SessionNotification`, v2 `session/update`), 0 violations. The Agent call and the subagent's command from the example, cancelled 4 s after the command's permission request was answered: | client | `main` | this PR | | --- | --- | --- | | v1, `_meta.terminal_output` | closing report `failed`, no `terminal_exit` | the same update carries `terminal_exit {exit_code: null, signal: null}` | | v1, AIR with `terminal_output` | not run on `main` (same code path) | `terminal_exit` with `exit_code: 1` | | v2 | `cancelled`, no terminal update | `terminal_update` with `exitStatus: {}`, then `cancelled`, before the root's `idle cancelled` | `npm run check`, `npm run build` and `npm run test:run` (2932 tests) pass. No golden file changes. With #1336 and #1339 merged into it, in any of the six orders, the branch merges without conflicts and all 2948 tests pass. ## Not covered - A call that only a permission request announced, whose tool use never streamed before it was closed, has no entry in the tool use cache. Its tool is unknown here, so its terminal stays open. This is the lookup that `AsyncTaskRuntime` uses too (`toolUseCache`, then `resolvedToolNames`). Covering it needs the tool name where the permission request emits the call. - The fallback for a tool result without a tool use (`toAcpNotifications`) still reports the status only, with no exit. - The end-of-turn closing is covered by a test only: Claude Code does not end a turn with an unanswered call on demand.v0.90.1-preview.3 |
||
|
|
ebf8b49fd6 |
fix: show nothing of a tool that cancelled work asks to run after the cancel was reported (#1334)
Closes #1061. ## Where #1061 stands The incident in #1061 was tool starts streamed after `session/cancel` that never got a final status, so a client's ledger of open tool calls kept them forever. Since that report: - **#1313** drops streamed text, thinking and tool calls of the stopped work once the client has been told that the work ended (the cancelled response on v1, `idle cancelled` on v2), while the window after the cancel is open. - **#1327** and **#1221** close the tool calls that streamed before that report, before it is sent. So a late streamed tool start no longer reaches the client. This PR adds regression tests for that, and closes the one path that still produced such a tool call. #1313 named it as not changed: a tool call that the stopped work creates outside the stream. ## The remaining path Example: the user sends a second prompt while the first runs, then cancels. Claude Code had already taken the second prompt off its queue, so it still runs it, and its answer writes a file. The consumer drops the stream and the messages of that answer (#1313). But Claude Code asks for the Write's permission on its control channel: - **`canUseTool`** created the tool call (`ensureToolCallEmitted`) and sent the permission request. AskUserQuestion's elicitation goes the same way. - **The v2 PreToolUse preview hook** created the tool call with the Write's patch. The tool's result is dropped too, so that tool call never ended: the ghost of #1061, now for tools that need a permission. ## The fix - **The consumer records each tool use it reaches,** at its streamed start or, for one that did not stream, at its complete message, with whether it dropped it. The condition is the stream's existing one, now `dropsStoppedWorkOutput`: the client was told that the stopped work ended, and the window after the cancel is open. - **`canUseTool` denies a dropped tool use without showing anything:** no tool call, no permission request, no elicitation. The model gets "The user cancelled this prompt." The **preview hook** sends nothing for it. - **The control channel runs ahead of the consumer,** which is why #1313 left this open. While a cancel's window may be open, `canUseTool` and the hook wait until the consumer reaches the tool use. Claude Code streams the tool use before it asks, so the consumer is usually there within a few ticks. Example: the next prompt's tool asks while the session still looks cancelled, because the consumer has not reached that prompt's echo yet. The wait sees the consumer reach the echo, which closes the window, and then the tool use, which it shows, so the request goes out. - **The wait is bounded:** 2 s, or until Claude Code withdraws the request. The stream never shows some tool uses, such as SandboxNetworkAccess's, and those then go out as before. - **Without a cancel, nothing changes,** not even timing: the check answers synchronously and adds no tick before the permission request. ## Evidence **Tests** (`src/tests/acp-agent.test.ts`, "output after a cancel"; `src/tests/acp-v2.test.ts`): - An abandoned prompt's Write: nothing reaches the client, and the tool is denied. Claude Code asks before the consumer reaches the tool use, between its streamed start and its message, after its message, or for a tool use that did not stream. The same on v2, with Claude Code running the registered PreToolUse hooks and then `canUseTool`. - The next prompt's tool, asked while the window still looks open: its request goes out once the consumer reaches it. - A tool use the stream never shows: the wait ends when the request is withdrawn (before it asked, or while it waits), or after the bound, and then the request goes out. - **Mutation checks:** removing each of the 11 rules fails at least one test. That covers the `canUseTool` check, the hook check, the wait, waiting instead of denying, the stream record, the message record, the stream's dropped flag, the abort exit, the aborted-signal guard, the timeout, and the synchronous answer. **Live, against Claude Code** (v1 and v2, `default` permission mode, every update checked against the schema): - After a cancel, the next prompt's Writes asked for their permissions at once and ran. Nothing was denied, and no tool call stayed open; on v2 the turn went `idle cancelled`, `running`, `requires_action`, `running`, `idle end_turn`. No schema violations. - The abandoned prompt's race did not happen live: Claude Code folded the queued prompt into the cancelled request, and the cancel stopped both. #1313 could not reproduce it either. The tests follow the order a direct CLI probe recorded for #1309. `npm run check`, build and the full suite (2931 tests) pass, with no golden changes. ## Not covered - On CLIs without `request_marker_lists_v1`, the window still lasts until the next prompt's echo, as before. - A tool that runs without a permission, in `acceptEdits` or `bypassPermissions` mode, still runs for an abandoned prompt. Nothing of it reaches the client, since its output is dropped. Stopping such tools would need the adapter to block tools it is not asked about, which this PR does not do.v0.90.1-preview.2 |
||
|
|
df21b3fb56 |
feat: restore a subagent from before a reload when SendMessage resumes it, for subagents RFD clients (#1332)
After `session/load` or `session/resume`, Claude Code can resume an agent that ran before the reload: a SendMessage to it runs it again from its transcript. The adapter did not know that agent, so it was never exposed, and its activity was dropped. Now it runs in its session from before, the agent id, for every subagents RFD client: v1 clients that declare `subagents` and are not AIR, and every v2 client. ## What Claude Code sends From a live run, after a resume in a new adapter process: - `task_started` with the agent id, the **SendMessage** call's id as `tool_use_id`, and the message as `prompt`. - The resumed run's messages under `parent_tool_use_id` = the agent's **original Agent launch id**, from before the reload (the `toolUseId` in `subagents/agent-<id>.meta.json`), not the SendMessage id. - The SendMessage result with `resumedAgentId`, then `task_updated` / `task_notification` as usual. The runtime found no child for the agent id, and a SendMessage call establishes no parent, so the child waited for a parent that never came and was withdrawn when it ended. ## What changes - **The signal:** an agent that the runtime does not know, resumed by a SendMessage call that it saw. A new launch never counts: its `task_started` names an Agent or Task call. - **`subagentLineage`** (`subagent-history.ts`) reads the agent and its ancestors from the session's history, root first. The metadata names each launch call but not who made it, so a launch's parent is the session whose transcript has the call: the root's, or an agent's. That is the rule the replay (#1328) uses, so live restore and replay agree. An agent or ancestor the history cannot place gives an empty lineage. - **The runtime** (`NativeSubagentRuntime.restore`) registers the agent and its ancestors under their agent ids and their launch ids. Then it announces, parent before child, each ancestor that the client was not told about in this connection, as the RFD requires before the agent's traffic. An ancestor is announced with no work state, since nothing says that it runs. Then the agent, as `running`. From there the resume goes through the existing path (#1321): the SendMessage text is the child's `session_message`, and its end is `idle`. - **Unchanged:** an agent the history cannot place stays unexposed, as the RFD forbids guessing a parent. - **Load and replay:** after `session/load`, the replay has already announced the agent. The live resume announces it once more, as `running`, which repeats an association the client has (the RFD allows it). - **Refactor:** `subagentLaunchIds` moves from `acp-agent.ts` to `subagent-history.ts`, so the replay and the lineage find launches the same way. ## Evidence **Live, against Claude Code, with every update checked against the schema** (no violations): | Run | Result | |---|---| | v1, `session/resume`, SendMessage to a root-level agent | `subagent_update` running under its old id, the message from the root, its `PING` in its own session, `idle end_turn` | | v1, `session/load`, same | The replay announces it; the live resume after the response runs in the same session | | v1, `session/resume`, root → coordinator → echoer, SendMessage from the root to echoer | coordinator announced under the root with no state, then echoer under coordinator as running, its run in its session, its `idle` on coordinator | | v2, `session/resume` without replay, same nested session | as above; echoer also reports `running` and `idle` as its own `state_update`, coordinator has none | Before this change, the first run showed no `subagent_update` at all, and the child's `PING` was dropped. **Tests:** - `native-subagents.test.ts`: restore under the agent id, with routing by the launch id; ancestors first and with no state; an ancestor announced once for two agents; output that waited is delivered; restore at the SendMessage result alone; an agent the history cannot place or read stays unexposed; no history read for a new launch or a known child. - `subagent-history.test.ts` (real transcripts on disk): a root-level agent, a nested agent next to a sibling, an agent whose launch no transcript has, a nested agent whose ancestor no transcript placed, an agent without metadata. - `acp-agent.test.ts`: the agent's own runtime restores an agent from a history on disk, from the stream Claude Code sends. - **Mutation checks:** removing each of the 12 rules fails at least one test. `npm run check`, build and the full suite (2922 tests) pass, with no golden changes. ## Not covered - **AIR** is unchanged: this PR changes only the RFD form, so on AIR an agent from before a reload still stays unexposed. - **A fork** cannot resume an agent of the original session, since the SDK's `forkSession` copies no `subagents/` history.v0.90.1-preview.1 |