838 Commits

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


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


### Features

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


### Bug Fixes

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

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: acp-release-bot[bot] <246668977+acp-release-bot[bot]@users.noreply.github.com>
v0.91.0
2026-10-15 12:22:03 +00:00
Ben Brandt
761b4c8359 fix: update Claude Agent SDK to 0.3.300 (#1355)
Updates `@anthropic-ai/claude-agent-sdk` from 0.3.296 to 0.3.300 (parity
with Claude Code v2.1.300), and `@anthropic-ai/sdk` from 0.133.0 to
0.134.0.

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

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

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

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

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

---------

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

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

## Why

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

## How

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

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

## Tests

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

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

---------

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

## The bug

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

## The fix

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

## Checks

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

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

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

## The bug

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

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

## The fix

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

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

## Checks

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

## What changes

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

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

## How

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

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

## Checks

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

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

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

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

## Not covered

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

## What Claude Code does

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

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

## The change

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

## Evidence

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

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

No tool call was left open in any run.

**Tests:**

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

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

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

## Not changed, and a decision

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

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

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

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

### Not covered

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

### Verification

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

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

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

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

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

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

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

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

## Cause

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

## Change

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

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

## Evidence

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

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

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

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

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

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

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

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

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

## Not covered

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

## The bug

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

## The fix

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

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

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

## Evidence

**Tests:**

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

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

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

## The bug

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

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

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

## The fix

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

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

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

## Evidence

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

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

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

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

## Not covered

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

## The bug

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

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

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

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

## The fix

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

## Evidence

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

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

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

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

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

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

## Not covered

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

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

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

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

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

## Change

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

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

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

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

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

## Tests

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

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

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

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

## Not covered

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

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

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

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

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

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

## Cause

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

## Change

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

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

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

## Evidence

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

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

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

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

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

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

## Not covered

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

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

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

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

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

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

## Change

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

## Evidence

**Tests** (6 new):

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

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

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

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

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

## Not covered, and a decision for the maintainer

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

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

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

## Cause

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

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

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

## Change

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

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

## Per client form

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

## Evidence

**Tests** (10 new):

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

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

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

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

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

## For the maintainer

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

## Not covered

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

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

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

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

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

## Change

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

## Evidence

**Tests** (8 new):

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

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

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

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

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

## Not covered

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

## Where #1061 stands

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

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

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

## The remaining path

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

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

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

## The fix

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

## Evidence

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

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

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

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

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

## Not covered

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

## What Claude Code sends

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

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

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

## What changes

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

## Evidence

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

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

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

**Tests:**

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

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

## Not covered

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


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


### Features

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


### Bug Fixes

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

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: acp-release-bot[bot] <246668977+acp-release-bot[bot]@users.noreply.github.com>
v0.90.0
2026-10-13 18:54:08 +02:00
dependabot[bot]
d6f60f512a feat(deps): Bump @anthropic-ai/claude-agent-sdk to 0.3.296 (#1330)
Bumps the minor group with 14 updates:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

---

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

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


</details>

---------

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

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

## What it fixes

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

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

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

## Changes

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

## Tests

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

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

## Not covered

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

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

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

## What it fixes

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

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

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

## What it does not fix

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

## Test changes

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

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

## Live check

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

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

https://claude.ai/code/session_019vH6BXrsYQN3MqQ93JgStf


</details>

---------

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


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

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

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

---

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

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


</details>

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

## The rules

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

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

## Changes in the code

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

## Evidence

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

## Not in this PR

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

## Release

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

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

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

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

## Cause

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

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

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

## Change

Two rules, in the stream consumer:

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

Supporting pieces:

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

## Why both rules

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

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

## What does not change

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

## Evidence

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

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

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

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

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

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

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

## Related, and not covered

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

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

## Experimental ACP v2

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

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

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

## What changes

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

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

## Live run

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

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

## Tests

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

## Release

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

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

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

AIR's golden files had recorded both.

## Cause

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

## Change

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

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

## Evidence

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

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

**AIR is unchanged.**

## What changes

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

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

## Live run

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

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

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

## Tests

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

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

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

## What changes

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

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

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

## Live run

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

## Tests

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

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

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

## Who gets what

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

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

## The RFD form

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

## Not in this PR

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

## Settled during the review of #1257

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

## Live run

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

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

## Tests

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

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

## Release

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

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

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

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

### Sanitized auth status

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

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

### Verification

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

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

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

### Scope

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

---------

Co-authored-by: Junie <junie@jetbrains.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Andrey Bragin <andrey.bragin@jetbrains.com>
v0.89.2-preview.15
2026-10-13 14:29:48 +02:00
Ben Brandt
30ad5f7372 refactor: send each tool call's _meta as the state a v2 client stores on the experimental ACP v2 surface (#1318)
## Problem

A v1 report's `_meta` belongs to that report, and the agent gives each
report its own `_meta.claudeCode` keys. A v2 client stores a tool call's
`_meta`, and any report that carries `_meta` replaces it
(`tool-calls.mdx`; the migration guide since
agentclientprotocol/agent-client-protocol#2355). So a v2 client kept
only the last report's keys. For example, a result's `{ toolName }`
replaced the denial reason that came before it. On top of that, a
PostToolUse hook stored the whole `tool_response` in `_meta`, which for
a Write is the whole file text.

## Change

All of this is v2 only. No v1 or AIR output changes, and no v1 golden
changes.

- **Stored `_meta` (`src/v2/tool-call-meta.ts`).** The v2 bridge keeps
each tool call's `_meta` the way the client stores it. `claudeCode` keys
merge one by one, and every other key replaces whole, the same model
`ToolCallFieldTracker` uses.
  - A report that changes the stored `_meta` carries all of it.
- A report that changes nothing carries no `_meta`, which leaves the
stored value unchanged.
- A permission request's subject always carries the stored `_meta`,
since a client can show the request on its own.
  - The tool call's first report starts from nothing.
- **`toolName` becomes `name`.** `claudeCode.toolName` repeats the
tool's name, which v2 has a field for. It becomes the `name` of a tool
call that has none yet, and is left out otherwise. A memory recall gets
a `name` this way. Subagent routing still reads `toolName` on the
agent's v1 reports, before the bridge.
- **The `toolResponse` AIR gets.** A v2 client also keeps `_meta`, so it
gets AIR's trimmed `toolResponse`:
- a hook carries only the `status` and `isAsync` markers, not output the
content already shows;
- a denial carries the SDK message only when it differs from the reason.

## Evidence

- **v2 goldens:** 28 change. Most only lose `toolName`. Hook reports
lose the repeated stdout or file text. Memory recall gains `name`. The
denial keeps its reason type and message. Subagent tool calls and their
permission subjects keep `parentToolUseId`.
- **Live, CLI 2.1.293 on Haiku, v2:** a Write and a Bash command arrived
with `name` on their first report and no `_meta` at all.
- **Tests:** 7 unit tests for the stored `_meta`.
- **Mutation checks:** removing any of these 9 rules fails at least one
test:
  - naming once;
  - leaving `toolName` out;
- merging with earlier `claudeCode` keys, which also fails the
`subagent-late-child-update` golden;
  - keeping other keys;
  - omitting unchanged `_meta`;
  - starting fresh;
  - the subject's stored `_meta`;
  - the bridge wiring;
  - the trimmed `toolResponse`.

Forgetting a closed session's tool calls is memory cleanup that no test
observes, like `V2Terminals.forget`. A limit of 2048 tool calls also
bounds the memory.
- `npm run check`, build and all 2820 tests pass.
v0.89.2-preview.14
2026-10-13 07:57:05 +02:00
Ben Brandt
383bc7dcf0 fix: start a forked session so the client can prompt it at once (#1317)
## Problem

The v1 schema says a forked session is active once the agent responds,
and can be prompted without `session/load` or `session/resume`
(agentclientprotocol/agent-client-protocol#2359). `session/fork` only
copied the transcript, so prompting the new id failed with "Session not
found" until the client loaded or resumed it.

## Change

- **The fork starts the new session the way a resume does.** It starts
from the copied transcript, with the model and the permission mode that
transcript records, and with the request's `cwd`, MCP servers,
additional directories and `_meta`. The response carries the session's
`modes` and `configOptions`, and the available commands follow it, as
for `session/new`.
- **A client that loads or resumes after a fork keeps working.** A later
`session/load` replays the history into the running session, and a later
`session/resume` returns it. Neither starts a second Claude Code
process. A load or resume with other settings restarts the session, as
for any loaded session. The fingerprint that decides this ignores the
AIR fork point in `_meta`.
- **Cost:** each fork now starts a Claude Code process. That added about
0.65 s to the fork response in the live run.
- **v2:** the v2 fork used to resume the forked session itself. It now
uses the session that the v1 fork starts, so the v2 bridge drops its own
resume.
- **AIR docs:** `docs/air-extensions.md` now says, under the session
fork point, what a load or resume after a fork does.

## Evidence

**Live, CLI 2.1.293 on Haiku, over v1:**
1. A new session answered "Remember the codeword PELICAN" with `OK`.
2. `session/fork` returned the fork with mode `auto` and the `mode`,
`model` and `effort` config options.
3. Prompting the fork right away returned `PELICAN`, with no load or
resume in between.
4. A `session/load` of the fork then replayed its history and was ready
in 4 ms. The timing log shows one session start for the fork and none
for the load, and a further prompt still answered.

**Tests:**
- New: the fork can be prompted at once, and its response carries modes
and config options; a later resume and load reuse the session; and the
fork fails when its session can't start.
- The existing fork tests now also check that the session starts from
the fork, with the request's additional directories.
- The v2 fork tests pass unchanged.
- **Mutation checks:** removing the start, the response's `modes`, or
the additional directories each fails a test.
- `npm run check`, build and all 2813 tests pass, with no golden
changes.
v0.89.2-preview.13
2026-10-13 05:28:48 +00:00
Ben Brandt
9db8a28b64 refactor: show an approved ExitPlanMode plan as a markdown plan on the experimental ACP v2 surface (#1314)
## Summary

When the user approves a plan through ExitPlanMode, a v2 client now gets
it as the session's markdown plan. The update uses the draft's unstable
plan operations:

```json
{ "sessionUpdate": "plan_update", "plan": { "type": "markdown", "planId": "plan-mode", "content": "# Plan…" } }
```

Until now a v2 client saw the plan only in the "Approve Plan" tool call
and its permission request.

## Behavior

- **Its own plan id.** The approved plan has the id `plan-mode`, and the
TodoWrite checklist keeps `plan`, so the client shows both. Claude Code
keeps one plan file per session and rewrites it on a later plan mode, so
each approval replaces the plan before it. Nothing removes it.
- **A normal approval** is a successful ExitPlanMode result. The plan
follows the tool call's result, live and on replay.
- **Live:** the plan comes from the tool's structured output, which
includes edits the user made to the plan.
- **Replay:** the SDK's `getSessionMessages` drops the structured
output, but the transcript keeps the `plan` that Claude Code adds to the
tool's input.
- **An approval that clears the context** ends the tool with an error
result. The restart into the fresh context publishes that plan, after
the new mode and config options and before the fresh context runs. It
does so only once that context exists, so a cancel or a failed restart
leaves no plan behind that was never carried out.
- **Not published:** a rejected plan, a subagent's plan
(`parent_tool_use_id` or `isAgent`), a plan still waiting for its team
lead, and an empty plan. Plans are trimmed.
- **Who gets it.** The agent sends plan operations only to a client that
advertises v1's draft `plan` capability, as the v1 draft requires. The
v2 bridge advertises it for every v2 client and passes `plan_update` and
`plan_removed` through unchanged, since the shapes match. No v1 client
advertises `plan` today, so v1 behavior doesn't change.

## Replay limits

- **After a "clear context" approval, replay doesn't show that plan.**
It reads the transcript from before the clear, where the approval is the
deny, so it shows the plan approved before that, or none. Replay never
reaches the conversation after a clear, so this is part of an existing
gap.
- **A plan approved before a compaction boundary isn't replayed**, like
the rest of the history before it.
- **Replay can't apply the subagent and team-lead checks**, since they
come from the structured output that `getSessionMessages` drops. A
subagent's transcript still carries its `parent_tool_use_id`, so it's
skipped.

## Evidence

**Live, CLI 2.1.293 on Haiku, through the adapter on v2 in plan mode:**
- **Normal approval:** the plan arrived right after "Exited Plan Mode",
and resuming with replay republished it.
- **"Clear context":** the plan arrived after the switch to auto mode,
before the fresh context's first tool call.
- **Raw SDK probe:** the structured result carries `plan`, `isAgent` and
`filePath`, and the transcript's tool use keeps `plan` and
`planFilePath`.

**Tests and checks**
- **Result path:** unit tests for each rule and for the order. They
include structured output versus input, input-only as on replay, and
structured output with no plan.
- **Restart path:** coordinator tests check that the plan comes after
the session state and before the continuation, that a client without the
capability gets none, and that a failed restart publishes none.
- **Agent:** a client that advertises `plan` gets the restart's plan,
and one that doesn't gets nothing.
- **v2:** a replay test and translation tests. The v2
`exit-plan-approve` golden gains the plan update. `exit-plan-reject` and
every v1 golden are unchanged.
- **Mutation checks:** removing any rule on either path fails at least
one test. That covers both capability checks, the agent's capability
wiring, success only, structured output first, the order, trimming, and
the `plan_removed` pass-through.
- `npm run check`, build and all 2810 tests pass.
v0.89.2-preview.12
2026-10-13 07:13:50 +02:00
Ben Brandt
1137df33aa fix: drop streamed output of prompts a cancel abandoned (#1313)
## Problem

After a cancel, the consumer drops the main conversation's consolidated
`assistant`/`user` messages until Claude Code starts a model request
that serves none of the cancelled prompts (#1309). `stream_event` frames
skipped that check. Claude Code always streams, so when it had already
taken an abandoned prompt off its queue, the prompt's answer still
streamed to the client as deltas. Only the consolidated copy was
dropped.

That output arrives after the cancel has been reported: on v1 after the
turn's `cancelled` response, and on v2 after `idle cancelled`, outside
any cycle. Both protocol versions require an agent's updates to come
before that report. Streamed tool calls leaked the same way.

#1309's tests missed this because they scripted only consolidated
messages.

## Change

- **After the client has been told the stopped work ended**, while the
window is still open, streamed text, thinking and tool calls are dropped
like the consolidated messages they belong to. On v1 that report is the
turn's prompt response (no active turn). On v2 it's the idle state (no
open foreground work). The v2 signal also covers cycles that Claude Code
starts on its own and turns that v1 holds for their subagents.
- **Before that report**, what the interrupt flushes still streams, as
it does today. Both drafts allow updates between the cancel and the
cancelled report, and the text is in Claude Code's transcript, so a
reload shows it anyway. Existing tests pin this: v2's queued-prompt
cancel, a held turn cancelled mid-follow-up, and a cancel that arrives
during a buffered backlog.
- **A prompt that is merely queued no longer keeps the window open.**
#1309 held the window while any queued prompt was unsettled. So a
follow-up that Claude Code ran ahead of a prompt sent after the cancel
lost its consolidated copy, and with this change it would have lost its
streamed copy too. Now a request holds the window only when:
  - the stopped turn is still unsettled;
  - an abandoned prompt's result is still expected;
  - the request names a cancelled prompt; or
- the request names an unsettled prompt. That covers one that arrived
while the cancel was still awaiting, before the cancel records it.
- **Per-block drop latch.** Where the consolidated message delivers the
text, a stream that loses a delta keeps dropping until its streamed-text
record resets: at the next message, or when a consolidated message
consumes the record. If a window closes mid-block (an older CLI's late
prompt echo), the record holds nothing, so the consolidated message
delivers the whole block once. Without the latch the record would hold
only the tail, which isn't a prefix of the text, and the client would
get the tail and then the whole text again. A client that gets subagent
text only streamed has no consolidated copy to fall back on, so its
subagent streams don't latch.
- Bookkeeping for dropped events (message id, usage, compaction
heartbeat) is unchanged.

## Evidence

**Tests**
- The "output after a cancel" suite now streams every answer the way
Claude Code does, with Claude Code's follow-up in two chunks. Without
the fix, 9 of its tests leak the streamed copy.
- New v1 tests:
  - a follow-up that runs ahead of a prompt sent after the cancel;
- a prompt sent during the cancel whose request arrives before the
cancel abandons it;
  - an echo in the middle of a two-block answer;
- a subagent stream for a client that gets subagent text only streamed.
- New v2 tests:
  - an abandoned prompt's streamed answer;
  - what the interrupt flushes of a cycle that Claude Code started;
- a held turn's follow-up that the cancel stopped before it showed
anything.
- **Mutation checks:** each of these fails at least one test:
- removing any of the window's four rules, or restoring #1309's hold for
any queued prompt;
  - removing the drop;
  - dropping the flushed tail before the report too;
  - using the v1 signal on v2;
- removing the latch, either of its record resets, or the subagent
condition.
The exception is clearing the latch when a subagent finishes. That's
memory cleanup that no test can observe, like the
`streamedBlocksByParent.delete` beside it.
- `npm run check`, build and all 2796 tests pass, with no golden
changes.

**Live, CLI 2.1.293 on Haiku, v1 and v2**
- A plain cancel mid-answer, and cancels with a background command
finishing during or after the cancelled turn.
- Nothing of the cancelled turn arrived after its report.
- Claude Code's follow-up after the cancel streamed in full. On v2 it
came in its own `running` … `idle end_turn` cycle.
- An abandoned prompt that Claude Code had already dequeued is a narrow
race through the adapter, so I didn't reproduce it live. The tests
follow the order a direct CLI probe recorded for #1309.

## Not changed

- A dropped prompt that runs a tool can still surface it after the
cancel, through `canUseTool` (tool call plus permission request) and the
PreToolUse preview hook. Neither checks the cancel. A check there would
read the consumer's state, which lags the CLI's control channel, so this
needs its own design.
- On CLIs without `request_marker_lists_v1`, the window still lasts
until the next prompt's echo. Claude Code's own follow-ups in that
window now lose their streamed copy as well as the consolidated one. The
bundled CLI has the marker.
v0.89.2-preview.11
2026-10-12 22:18:38 +02:00
Ben Brandt
d074558e4a fix: keep the terminal of a command in the background running until its task ends (#1312)
A Bash call that sends its command to the background returns as soon as
the command detaches, and its tool result carried `terminal_exit`. So a
client that renders terminals, whether Zed's terminal extension or a v2
display terminal, showed the command's terminal as exited, with no code,
at launch, while the command ran on.

## What changes

For a client that renders terminals and isn't AIR:

- **At launch:** the tool result of a command in the background leaves
the exit out (`withRunningTerminal`). The tool call still completes; the
v2 draft keeps "Terminal exit and tool call status are independent".
- **When the command's task ends:** the consumer sends `terminal_exit`.
It does so at the task's `task_notification`, or at its terminal
`task_updated` if that comes first. The update is routed like the tool
call, so a subagent's command ends in the subagent's session.
- **No exit code.** Claude Code names the code only in the
notification's summary text ("…failed with exit code 3"), which the
adapter doesn't parse. `completed` doesn't mean 0 either: Claude Code
accepts some non-zero codes as a success, such as grep's 1.

The terminal still exits at the tool result, as before, in four cases:
- **A fast command** whose task ended before its tool result.
- **A task that started after the tool result.** Claude Code sends
`task_started` first, but the existing `bash-background` scenario
scripts the reverse, so its goldens don't change.
- **AIR,** which shows the command through its async tasks.
- **Replay,** where the command from before the reload no longer runs,
and the structured tool result isn't available anyway.

## Evidence

- **Live, CLI 2.1.293, through the built adapter over ACP:**
- **v2:** the terminal stays running when the Bash call completes and
the turn goes idle. `terminal_update` with `exitStatus: {}` arrives when
`sleep 5` finishes, before Claude Code's follow-up.
- **v1 with `terminal_output`:** no exit at the Bash result;
`terminal_exit` with code `null` arrives when the command finishes.
- **Recorded, CLI 2.1.293:**
- the order is `task_started` (`local_bash`, `is_backgrounded: true`),
then the Bash result with `tool_use_result.backgroundTaskId`, then
`task_updated` and `task_notification` when the command ends;
  - neither event carries a structured exit code.
- **Tests:**
- v2: the terminal runs until the task ends, with one exit after the
turn's `idle`; and a task that ended before the result exits at the
result.
- v1 with Zed's terminal extension: the exit comes after the prompt's
answer. AIR keeps its exit at the result.
- Unit tests for `withRunningTerminal`, including dropping a `_meta`
that would be left empty, since an empty `_meta` would clear what a v2
client stores.
- **Mutations:** removing the strip, the end-of-task exit, the liveness
guard, or the AIR guard each fails a test.
- `npm run check`, build and the full suite pass, with no golden file
changes.
v0.89.2-preview.10
2026-10-12 20:53:07 +02:00
Ben Brandt
14b2233192 refactor: leave requests of agents without a record out of requires_action on the experimental ACP v2 surface (#1311)
A follow-up to #1310, from its review, which found no regressions but
two wrong answers. Both come from one choice in #1310: a request from an
agent without a `liveBackgroundTasks` record put v2's foreground work in
`requires_action`.

**When an agent has no record:**
- **A teammate:** it never gets one.
- **A settled subagent:** settling deletes its record, and settling
didn't re-check open requests.
- **A foreground subagent nested in a background one:** Claude Code
leaves it out of `background_tasks_changed`, so the level reconciliation
prunes it after two activations.

The main conversation waits on none of them. A background subagent's
request that the consumer read before its `task_started` also flickered
from `requires_action` to `running`.

## What changes

- **An agent without a record no longer blocks the foreground work.** A
subagent the main conversation waits on has a record by the time it
asks: its `task_started` comes first on the wire, and nothing prunes it
while the main turn waits. When the consumer reads the request first,
the `task_started` checks again, so `requires_action` comes slightly
later instead of a flicker for background subagents.
- **Settling a task re-checks** the open requests.
- **The comment on `nativeSubagentParentByToolUseId`** no longer claims
`parent_task_id` is unread.

## Tests

- **Replaced:** "stops waiting for a request once its subagent turns out
to run in the background". The new test checks that a request read
before its `task_started` doesn't block, then blocks once the
`task_started` says foreground.
- **New:** a foreground subagent's open request stops blocking once
Claude Code stops that subagent.
- **Mutations:** restoring "no record blocks" fails both tests; dropping
the re-check at settle fails the second.
- `npm run check` and the full suite pass, with no golden file changes.
v0.89.2-preview.9
2026-10-12 20:23:46 +02:00
Ben Brandt
c22ec1d189 refactor: report requires_action only for requests that block the foreground on the experimental ACP v2 surface (#1310)
v2's foreground work reported `requires_action` whenever any permission
request or question in the session was open, including a background
subagent's. The draft keeps `requires_action` for foreground work that
is blocked on user action, and a background subagent asks without
blocking the main conversation. So a v2 client saw a running turn as
waiting on the user while it kept working.

## What changes

A request blocks the foreground work when it is:
- the main conversation's own request, or
- the request of a subagent that runs in the foreground of the main
conversation, so the tool call that spawned it waits on it. This applies
up a chain of such subagents.

A request from a subagent in the background, or from one nested inside
it, no longer counts.

- **Where each task runs.** The adapter records this per task:
  - from `task_started.is_backgrounded` and `parent_task_id`;
- from a later `task_updated` patch that moves the task to the
background;
  - Claude Code always resumes a subagent in the background.
- **Unknown subagents.** A subagent without a record still counts,
because its request can arrive before its `task_started`. Registering a
task, or moving one to the background, checks the open requests again.
- **Requests that name no agent** still count: MCP elicitations and
dialogs.
- **Steering is unchanged.** It still treats any open request as
blocking. `Session.pendingUserInputCount` becomes
`Session.openUserInputs`: the open requests, each with the subagent that
asked.
- **v1 is unchanged.**

## Evidence

- **Live, CLI 2.1.293 via the SDK with a logging `canUseTool`:**
- `task_started` carries `is_backgrounded: true` for a background
subagent and `false` for a foreground one.
- A subagent's `canUseTool` call carries `agentID` equal to its
`task_started.task_id`.
  - `task_started` arrived before the subagent's request in both cases.
- `parent_task_id` for nested subagents was not checked live; it's
documented in the SDK types and covered by a test.
- **6 new v2 tests:**
- a background subagent's permission request leaves the turn `running`,
and so does its question;
  - a foreground subagent's request reports `requires_action`;
  - a foreground subagent launched by a background one doesn't block;
- a request that arrives before its subagent's `task_started` stops
blocking once the subagent turns out to run in the background;
- a request stops blocking once Claude Code moves its subagent to the
background.
- **Mutation checks.** Removing each rule in turn fails at least one
test. That includes passing the subagent through for permission requests
and for questions, the recheck at registration and at `task_updated`,
and the walk up the chain.
- `npm run check`, build and the full suite pass, with no golden file
changes. Two resume tests now expect the resumed task to be recorded as
backgrounded.
v0.89.2-preview.8
2026-10-12 20:08:01 +02:00
Ben Brandt
b007ac7c45 fix: end a cancelled turn at its interrupted result and deliver Claude Code's follow-up after a cancel (#1309)
After a cancel, Claude Code ends the interrupted turn with an aborted
result, then keeps working through its queue, and reports `idle` once at
the end. The queue can hold a prompt it had already taken off it, then
its own follow-ups, such as the summary of a background command that
finished. The adapter settled a cancelled turn only at that final
`idle`, and dropped everything in between until the next prompt
activated. As a result:

- **v1:** a cancelled `session/prompt` waited for any follow-up queued
behind it to finish.
- **v2:** a follow-up after a cancel streamed as text with no `running`
or `idle`.

This PR ends the cancel where Claude Code's own messages say the stopped
work ends.

## What changes

- **Where a cancelled turn settles.** It now settles at the result of
the work the interrupt stopped: its own result, or, for a turn held open
for its subagents, its follow-up's aborted result.
- The `idle`, the next echo's hand-off and the force-cancel backstop
still settle a turn whose result never comes.
- That result owes its trailing `idle` like any settling result, a
steered turn's included.
- **When the cancel ends.** The cancel records the prompts it stopped:
the unsettled turns and their steers when it arrives, and whatever it
abandons when it sweeps the queue.
- With `request_marker_lists_v1`, every `status: requesting` frame lists
the prompts its model request serves. A cycle Claude Code starts on its
own lists none.
- The cancel ends at the first request that serves none of the recorded
prompts, once no turn is unsettled and no orphan result is outstanding.
Compaction updates resume there too.
- A prompt Claude Code had already dequeued names itself, so its answer
stays dropped even when the interrupt receipt leaves it out.
- Without the capability, the cancel ends at the next activation, as
before.
- **Abandoned prompts' results.** A result that names a cancelled prompt
never promotes the next queued prompt, so a prompt sent after the cancel
can't take an abandoned prompt's result.
- **Held turns.** Since CLI 2.1.270, Claude Code reports `running` for
as long as background agents live. So with request markers, a held
turn's follow-up counts as live only when a model request has started
since the turn's result. A cancel while only the subagent runs settles
the held turn at once.
- **v2: aborted cycles.** A cycle whose result is aborted
(`terminal_reason` `aborted_streaming` or `aborted_tools`) ends
`cancelled`.

## Evidence

**What CLI 2.1.293 sends.** I recorded the stream live with a probe that
drives the SDK the way the adapter does. After an interrupt:
1. The interrupted turn's aborted result comes first.
2. Next, any prompt Claude Code had already taken off its queue runs.
Its request names it.
3. Then its own follow-ups run. Their requests name no prompt.
4. Finally, one `idle`.

The interrupt stops background agents, but not background commands.

**An audit of the bundled CLI** found exactly one source of `status:
requesting` frames: one per main query-loop iteration. Subagents,
compaction, side queries, hooks and retries emit none, or only a named
one.

**The adapter, end to end over ACP against the live CLI**, this branch
against `main`:

| Scenario | `main` | This PR |
| --- | --- | --- |
| v1: a background command finishes while the turn the user cancels
streams | `session/prompt` answered 14.1 s after the cancel | answered 4
ms after the cancel; the follow-up's text arrives after |
| v2: same | follow-up text with no `running`; `idle cancelled` 14.8 s
after the cancel | `idle cancelled` 10 ms after the cancel, then
`running`, the follow-up, `idle end_turn` |
| v2: a background command finishes after the cancelled turn | `idle
cancelled`, then text with no `running` or `idle` | `idle cancelled`,
then `running`, the follow-up, `idle end_turn` |
| v2: cancel while idle, then a background command finishes | text with
no `running` or `idle` | `running`, the follow-up, `idle end_turn` |
| v1: cancel while a background subagent holds the prompt open | (not
compared) | answered 2 ms after the cancel |

## Tests

- **20 new tests, scripted on the recorded orders.**
- v1 suite "output after a cancel" (12): a cancelled turn and a cancel
while idle; a dequeued prompt the receipt leaves out; a steer; a prompt
that Claude Code folds into its own follow-up; a prompt sent while the
cancel is still in progress; a repeated cancel; trailing idles after a
cancelled turn and after a steered one; older CLIs without request
markers.
- Held turns (3): a cancelled held turn settles at its follow-up's
aborted result, not at a placeholder result, and at once when only its
subagent runs.
- v2 (5): the follow-up after a cancelled turn as its own cycle; a
cancel while idle; a later cycle after a cancelled one; a cycle Claude
Code started before the cancel arrived; a follow-up's compaction.
- **Mutation checks.** Removing each rule in turn fails at least one
test. Two exceptions:
  - Clearing the record at activation can't be observed.
- Holding for any unsettled turn, not only a recorded one, can't be told
apart from activation.
- `npm run check`, build and the full suite pass, with no golden file
changes.

## Review

The design went through two rounds of independent review.

- **The first attempt, rejected.** It judged "the stopped work is over"
from the adapter's own bookkeeping. Review found that the interrupt
stops what Claude Code is running, which the adapter may not have seen
yet. That attempt is not in this PR.
- **This design.** A second round audited the CLI's signals and the
state machine. This PR fixes what it found:
- a prompt sent after a cancel could take an abandoned prompt's result;
  - a held turn could settle at a placeholder result;
- the window could close while an abandoned prompt that Claude Code
folds into its own follow-up was still due;
  - a prompt sent during the cancel's awaits wasn't recorded;
  - a held turn's follow-up was misread as live on CLI 2.1.270+.

## Known limitations

- **The cancelled turn's last streamed text still arrives after the
cancel**, as on `main`: the cancel never held streamed text back.
- **If the cancel lands just after Claude Code started its own
follow-up**, the interrupt stops that follow-up, and its partial output
gets through. On v2 it ends `cancelled`.
- **Trailing-idle debt can be over-counted by one in a drain run.** The
next `running` transition sweeps it, as before.
v0.89.2-preview.7
2026-10-12 19:17:16 +02:00
Ben Brandt
412da2789a refactor: report idle at a turn's result and Claude Code's own model cycles on the experimental ACP v2 surface (#1305)
Stacked on #1304, which this targets; it retargets to `main` once #1304
merges. Together they replace #1303.

## What a v2 client gets now

**`idle` at a turn's result,** also when v1 holds the turn's answer for
the background subagents it spawned (`Turn.deferredSettle`).
- The subagents' output and permission requests arrive while the session
is idle. The draft allows that: background activity may send updates
while idle, and they don't change the state.
- A background request while idle no longer shows `requires_action`,
which the draft reserves for foreground work.
- The hold itself is unchanged. It now only delays v1's answer, with no
v2 branch in the hold logic.

**Model cycles that Claude Code starts on its own** are foreground work:
the follow-up after a background subagent or command finished, or its
answer to a message from another session.
- A cycle reports `running` before its first output, `requires_action`
while it waits, then `idle` with its own stop reason, usage and
`_meta.quota`.
- An error result gives `idle` with the `error` stop reason, and a
cancel gives `idle cancelled`.
- Before this, such a cycle streamed while v2 said `idle`.

**Take-overs:** a prompt that Claude Code takes in during a cycle takes
it over, with no new `running` and no `idle` in between. So do a steer
that resumes a held turn, and the continuation of an accepted
clear-context plan. In both, the final `idle` carries the turn's usage,
not just the last cycle's.

v1 and AIR clients are unchanged.

## How

All of this builds on #1304's `ForegroundEvents`:

- **A turn's work ends at its result:** `endForeground` in the defer
branch of `settleOrDefer`, and where the steer idle lane re-applies a
hold.
- **A cycle starts** (`startAutonomousCycle`) at model output of the
main conversation while no work runs.
  - Subagent output doesn't count.
- Without command lifecycles, output while a prompt is queued stays that
prompt's, since its output can come before its echo. With lifecycles
(`msg_lifecycle_v1`), a prompt Claude Code hasn't started has no output
yet, so a cycle starts. This narrows the gap that #1303 left.
- **A cycle ends:**
- at a result no turn took, except a placeholder result (`num_turns:
0`);
- with `cancelled` at the diagnostic of an ExitPlanMode answer, as a
turn does, unless a held turn continues the accepted plan;
  - at an SDK `idle` that no earlier result owes;
  - when the stream ends or fails;
  - before `session/close` answers.
- **Starting work while a request is open** reports `requires_action` at
once.
- **The cycle's stop reason** comes from its result, as for a turn
(`autonomousCycleEnd`). Its `_meta.quota` uses the same builder as a
turn's (`quotaMeta`, extracted from `turnQuotaMeta`).

## Checks

- `npm run check`, `npm run build` and all 2756 tests pass. No golden
file changes against #1304: no recorded scenario has a held turn or
output while no turn is active.
- New tests:
- **14 v2 tests:** #1303's eleven, plus a held turn reporting `idle` at
its result with its follow-up as a cycle and a background request while
idle, output before a queued prompt that Claude Code reports it hasn't
started, and `session/close` during a cycle;
- **agent:** a steer resuming a held turn's work; the follow-up that
ExitPlanMode's "keep planning" interrupts ending `cancelled`; the
clear-context continuation taking the cycle over, with the turn's usage;
- **turn events:** work that starts while a request is open waits at
once.
- I made 20 mutations, one per rule above, and each failed a test. The
take-overs for a steer and for the clear-context restart first survived,
since a cycle stood in for the turn with the same states. The tests now
check the usage of the final `idle` too.

## Not covered

- No recorded scenario has such a cycle. The start signal (the first
main-thread output) is chosen from the agent's side, not confirmed
against a live CLI.
- After a cancel, the agent drops main-thread output until the next
prompt (`session.cancelled` stays set). So a follow-up that Claude Code
runs after a cancel is still lost, for v1 and v2. That predates this PR,
and I'll fix it separately.
v0.89.2-preview.6
2026-10-12 14:28:15 +02:00
Ben Brandt
2a8417ab89 refactor: report a session's foreground work apart from the prompt's answer on the experimental ACP v2 surface (#1304)
The first of two PRs that replace #1303, after a design review. This one
restructures how the agent reports v2's state, with two small changes in
behavior (below). The next PR makes v2 report `idle` at a turn's result
and adds model cycles that Claude Code starts on its own.

## Why

The agent reported each turn's v2 states (`running`, `requires_action`,
`idle`) through the prompt's `TurnEvents`, the same events that answer
the prompt. That ties v2's state to when **v1** gets its answer. The
clearest case is the hold (`Turn.deferredSettle`): v1 holds a turn open
while the background subagents it spawned run, because a v1 client stops
listening at the prompt response. So v2 stays `running` too.

Making v2 report `idle` at the turn's result in that design would need a
v2 branch inside the hold logic. That's the most intricate code in the
adapter: steering, cancel, the echo hand-off, ExitPlanMode, idle debts.
And a cycle that Claude Code starts on its own would need a second
source of v2 state beside the turn's (#1303).

## What changes

- **`TurnEvents` only answer the prompt:** `inserted`, `ended`,
`failed`. v1 answers at `ended` or `failed`. v2 answers at `inserted`,
and uses `ended`/`failed` only to reject a prompt before insertion.
- **New `ForegroundEvents`** (`src/turn-events.ts`): the session's
foreground work, meaning what a client would wait for before sending the
next prompt. Events: `running`, `awaitingUser`, `ended`, `failed`.
- Only the v2 surface supplies them, through the new `foreground`
option. They map to `state_update` in `v2Foreground`.
- A prompt's turn is the foreground work from its insertion until it
ends, exactly as before. So `idle` still waits for a held turn's
subagents; the next PR changes that.
- **The work is keyed by the session id the client knows,** not stored
on a `Session`. A clear-context restart (an accepted ExitPlanMode with a
fresh context) replaces the Claude Code session behind that id and moves
the running turn to it. Work stored on the old `Session` would stay
behind.
- **Requests report through the work** (`syncAwaitingUser`), not through
the active turn.

## Observable changes on v2

1. **The `session/prompt` response now comes before `running`, not after
it.**
- The 38 v2 golden files change by exactly that, and nothing else; I
checked every file with a script.
- The old order was not guaranteed: the SDK's `notify` writes after
`waitUntilInitialized(...).then(...)`, and the response is written once
the handler's promise resolves. The old code won that race by sending
`running` before resolving.
- The draft lets clients receive the updates before or after the
response.
2. **A request that Claude Code opens for a prompt it started, before
the prompt's echo, now reports `requires_action` when it opens,** rather
than at the echo. The prompt's work is already running then.

v1 and AIR clients are unchanged.

## Checks

- `npm run check`, `npm run build` and all 2739 tests pass.
- Every v2 state test in `acp-v2.test.ts` passes unchanged.
- `turn-events.test.ts` now reads the waiting events from the
foreground.
- New tests:
  - a request opened before the echo;
- the work staying with the session id across a clear-context restart,
with a permission request in the continuation.
- I made six mutations and each failed a test: no `running` at
insertion, no end on `resolve`, no end on `reject`, waiting never
reported, waiting only for the active turn, and the work tied to the
`Session` object.
v0.89.2-preview.5
2026-10-12 14:23:37 +02:00
Maksim Zheltoukhov
fe8b69edd4 feat: accept IDE session hooks without dropping model settings (#1265)
IDEs can pass their own Claude hooks through
`_meta.claudeCode.options.settings.hooks` on `session/new` and
`session/load`. Before this change, any session settings replaced
`CLAUDE_MODEL_CONFIG` entirely, so adding a single hook dropped the
deployment's `modelOverrides` / `availableModels`. The two are now
merged, and the session settings win per key.

The wrapper does not merge hooks itself: Claude Code already runs the
hooks of every settings tier, so user and project hooks keep running
next to the session ones. Hooks stay scoped to their session, and a
`session/load` with different settings recreates the query, which
replaces or removes them.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
v0.89.2-preview.4
2026-10-12 13:00:07 +02:00
Ben Brandt
b3bed2f763 fix: report a command whose result says interrupted as cancelled on the experimental ACP v2 surface (#1301)
#1289 made a v2 client get `cancelled` for a run that Claude Code
aborted, when Claude Code reports it as an error result with
`non_execution_kind`. Claude Code has a second way to report an aborted
command: a **successful** Bash result whose structured output says
`interrupted: true`. A v2 client got `completed` for that. With this PR
it gets `cancelled` too, so the rule is the same however Claude Code
reports the abort.

## When Claude Code sends that result

I read the CLI that SDK 0.3.293 bundles. I didn't reproduce these cases
live.

| What stops the command | What the adapter receives | v2 before | v2
after |
| --- | --- | --- | --- |
| The client cancels (`session/cancel`) | an error result,
`non_execution_kind: "interrupted"` | `cancelled` (#1289) | same |
| A timeout | an error, "Command timed out after …", **not**
`interrupted` (or the command is backgrounded) | `failed` | same |
| The output limit or a replaced output file | an error with `Exit code
137`, not `interrupted` | `failed` | same |
| A `now`-priority message such as `_session/steering`, landing just
before the command starts, or while the output limit kills it | a
successful result, `interrupted: true` | `completed` | **`cancelled`** |

A successful `interrupted` result needs the abort reason `interrupt`,
which a `now`-priority message causes. That abort doesn't kill a running
command, so this result only comes from the races in the last row.
Clients will rarely see it. The point of the change is that v2's status
follows what Claude Code reports, however it reports it.

v1 and AIR clients are unchanged: `completed` with no exit code, and AIR
still gets exit code 1.

## How

- `AcpToolCallRenderer.resultStatus` takes the command's `interrupted`
fact alongside `nonExecution`.
- `result()` used to call `resultFields`, which drops the command facts.
It now computes the facts once (`resultFacts`) and builds the fields
from them (`fieldsOfResult`). That lets it pass `interrupted` without
adding a field to `RenderedResult`, which the public
`toolUpdateFromToolResult` returns.
- The agent's checks for a finished tool call already count `cancelled`
(`isFinalToolCallStatus`, #1289).

## Checks

- `npm run check`, `npm run build` and all 2730 tests pass. No golden
file changes: no recorded scenario has an interrupted command.
- New tests:
- **renderer:** an interrupted command on v2, v1 and AIR, and one that
isn't interrupted on v2;
- **v2 surface:** a v2 client receives `cancelled`, and the turn still
ends `end_turn`.
- I made four mutations and each failed a test: the rule ignores
`interrupted`, every client gets `cancelled`, `result()` doesn't pass
the fact, and `result()` inverts it.
v0.89.2-preview.3
2026-10-12 12:15:06 +02:00
Ben Brandt
77910dea74 refactor: report a tool call that Claude dropped before it ran as cancelled on the experimental ACP v2 surface (#1300)
v2 has a `cancelled` tool call status ("the tool call was cancelled
before it completed"), which v1 doesn't. #1289 started using it for a
run that Claude Code aborted. This PR uses it for the other tool call
that never ran: one that Claude dropped before running it.

## What changes

When a turn ends with `end_turn` and a streamed `tool_use` never reached
a complete assistant message, Claude dropped that tool call before it
ran, for example when it switched to a steering message. The agent
closes it at the end of the turn with "Claude stopped this tool call
before it ran." The turn itself still succeeds.

| Case | v2 client | v1 and AIR clients |
| --- | --- | --- |
| Dropped before it ran | `cancelled` (was `failed`) | `failed`, as
before |
| Ran, but Claude Code returned no result | `failed`, as before, and the
turn fails with `incomplete_tool_call` | same |

The content text is unchanged in both cases.

## How

- **The renderer builds both closing reports**
(`AcpToolCallRenderer.unfinished`), and picks the status from
`ClientCapabilities.v2`, as `resultStatus` does since #1289. Before, the
agent built the report inline with a hardcoded `failed`.
- **The agent** passes whether the tool ran. Its closing step
(`failToolCall`, now `closeUnfinished`) already removes the call from
all of its tracking itself, so no "is this status final" check needed
changing.

## Checks

- `npm run check`, `npm run build` and all 2728 tests pass. No golden
file changes: no recorded scenario drops a tool call.
- New tests:
  - **renderer:** dropped and ran-without-result, on v2, v1 and AIR;
- **v2 surface:** a v2 client receives `pending` then `cancelled` for a
dropped tool call, and the turn ends `end_turn`.
- The existing v1 tests in `incomplete-tools.test.ts` are unchanged and
still pass.
- I made five mutations and each failed a test: the renderer never
cancels, the renderer cancels a tool that ran, the agent marks a dropped
call as having run, the agent marks a no-result call as dropped, and the
agent's renderer ignores the client's capabilities.
v0.89.2-preview.2
2026-10-12 11:57:21 +02:00
Ben Brandt
231bbd7dd6 refactor: show an Edit's or Write's exact patch before it runs on the experimental ACP v2 surface (#1291)
A v2 diff can't hold the snippet of an Edit's input (it needs line
numbers and the operation), so on v2 an auto-approved Edit or Write
showed its change only once the PostToolUse hook reported it, after the
tool had already run. Only an asked one showed its approval patch first.

## What changes, v2 only

The tool call shows the exact patch before the tool runs, auto-approved
or not.

## How

- **A PreToolUse hook for Edit and Write** (`fileChangePreviewHook`),
registered only on v2 connections. Claude Code runs the tool only after
the hook returns, so the file is still unchanged when the hook reads it.
That's the same guarantee the approval patch in `canUseTool` relies on.
The hook sends the same patch (`previewPatchContent`):
- **Stream already showed the tool call:** a `tool_call_update` through
the session's stream router, pinned like an approval patch.
- **Hook came first:** the SDK dispatches hook callbacks as soon as it
reads them, ahead of the message queue the agent consumes. The hook then
emits the tool call with the patch via `ensureToolCallEmitted`, the
permission flow's fix for the same race (#851).
- **Subagent tools** are attributed exactly as in `canUseTool`. That
rule moves into `toolUseOwner`, which both use, with no behavior change.
- **Safe failure:** the hook makes no permission decision, and any
failure only leaves the preview out.
- **v1 and AIR** register no PreToolUse hook, so Claude Code waits on
nothing new there and their output is unchanged.

I first tried hanging the preview on the per-tool-use hook registry that
PostToolUse uses. That fails when the hook comes before the stream, so
the hook lives on the agent instead.

## Golden files

The scenario harness now runs PreToolUse before each tool, as Claude
Code does, and applies each hook's `matcher`. The v2 golden files of the
four file-change scenarios change:
- **`write-existing`** (auto-approved): the patch now arrives before the
tool completes, and the final hook report no longer repeats it, because
the tracker sees no change.
- **`write-new`, `edit-with-permission`, `edit-rejected`:** the tool
call gets the patch just before the permission request, which carries
the same patch.

No v1, AIR or plain golden file changes.

## Checks

- `npm run check`, `npm run build` and all 2497 tests pass.
- A new v2 test runs an auto-approved Edit with the hook both after and
before the streamed tool use. It checks that the patch is computed from
the file before the edit, arrives before the result, and isn't replaced.
- A new test checks that only a v2 session registers the hook, with the
`Edit|Write` matcher.
- Mutations that fail tests: no hook on v2, the hook on v1 too, the
hook-first path doing nothing, the already-shown path sending nothing,
and Edit only.
- Not pinned by any test: the pin itself. On v2 nothing later in the
stream carries content for an Edit or Write, so it's only there for
consistency with the approval patch.

## Follow-up

AIR has the same gap for an auto-approved Write: it shows no diff until
the hook. That's out of scope here, since it changes AIR's output.
v0.89.2-preview.1
2026-10-12 11:31:13 +02:00
acp-release-bot[bot]
4e1fc1e8df chore(main): release 0.89.1 (#1294)
🤖 I have created a release *beep* *boop*
---


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


### Bug Fixes

* push renamed sessions to list subscribers and reuse one listing across
pages
([#1293](https://github.com/agentclientprotocol/claude-agent-acp/issues/1293))
([92abb8f](92abb8fd4d))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: acp-release-bot[bot] <246668977+acp-release-bot[bot]@users.noreply.github.com>
v0.89.1
2026-10-10 13:10:02 +04:00
nikita-ashihmin
92abb8fd4d fix: push renamed sessions to list subscribers and reuse one listing across pages (#1293)
Two findings of the 2026-10-10 night perf run on a 5,000-session home
(findings 8 and 9).

## Rename push (finding 9)

The run measured "no push within 10 s" after `_session/rename`. The
cause was the harness, not the adapter. The bench renames the newest
session to `"Night renamed"`, and an earlier bench run on the same home
had already given it that title. The transcript shows that pair of title
records twice. A rename to the current title appends nothing, so there
was nothing to push. On a fresh home, the same bench gets the push. The
hypotheses checked:

- The metadata cache is not updated by a rename. The appended records
change the transcript's size, and the watcher re-reads it.
- The sidecar lies outside the non-recursive watch, but so does every
rename. Each one also appends to the transcript, which is watched.

Still, the push depended on `fs.watch` plus its 150 ms quiet period. Now
a rename, archive, unarchive or delete through this connection publishes
at once. `ListSubscriptions.sessionWritten` stats the session's
transcripts and reads its row, the way a transcript event would, without
waiting. The event that follows finds the row unchanged and sends
nothing. Renames by other processes still arrive through the watcher.
The 1 s per-session interval of a subscription still applies.

## Listing every page (finding 8)

Profiling `SessionIndex.list` showed where a page's time goes. The full
enumeration and stat of 5,000 transcripts takes about 20 ms per page.
Most of the remaining ~380 ms was the SDK `getSessionInfo`, called once
per row read: its lookup runs a `readdir` of the whole project directory
(5,000 entries) on every call.

- A metadata read no longer calls the SDK. Titles come from the head and
tail the index already reads, by the same rule (`sdkTitles`). The tail
is searched as the SDK searches it, cut first line included.
- The pages after a cursor reuse the list's enumeration. It is keyed by
the cursor and kept for at most 30 s, at most 8 lists. It is dropped as
soon as a directory of the scope adds or removes an entry, through the
`DirListings` identity check. Such a page stats only the transcripts it
reads (each with its session's other copies) right before reading them.
A transcript that changed since either keeps its order key (no new
prompt) or moves up before the cursor, so the page is as fresh as one
from a new enumeration. The first page always enumerates.

## Measurements

Real adapter (`dist/index.js`), driven as AIR drives it by
`acp-perf-harness/air-ui/night/acp-adapter-bench.mjs`. Fixture: 5,000
sessions from `gen-night-data.mjs`.

| | before | after |
|---|---|---|
| Listing every page (25 × 200) | 10.1–16.3 s | 0.92–1.16 s |
| First page (51) | 156–472 ms | 46–62 ms |
| Show more (50) | 117–283 ms | 13–23 ms |
| Push after archive | ~170 ms | 3–4 ms |
| Push after a rename | ~170 ms | 3–5 ms |

## Tests

- A rename, archive, unarchive and delete each push within 500 ms while
the watcher's quiet period is 60 s (fails without the fix). A rename by
another process is pushed through the watcher.
- Paging:
  - one enumeration for all pages;
- between pages, a transcript emptied, one with a new prompt, and one
with only a new answer give the same pages as a new enumeration;
- an older copy of a session that got a prompt hides it on later pages;
  - adding or removing a transcript enumerates again.
- A custom title whose record starts before the tail window matches the
SDK.
- Read-count assertions use the new `SessionIndex.transcriptsRead`
counter instead of SDK call counts.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
v0.89.1-preview.1
2026-10-10 13:06:40 +04:00
acp-release-bot[bot]
96b6b4f478 chore(main): release 0.89.0 (#1279)
🤖 I have created a release *beep* *boop*
---


##
[0.89.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.88.0...v0.89.0)
(2026-10-09)


### Features

* add the AIR sessionIndex extension for fast session lists
([#1268](https://github.com/agentclientprotocol/claude-agent-acp/issues/1268))
([03d84f6](03d84f6663))
* **deps:** Bump @agentclientprotocol/sdk to 1.8.0
([#1287](https://github.com/agentclientprotocol/claude-agent-acp/issues/1287))
([446a721](446a7217fa))
* Support custom instructions for agent sessions via meta
([#1177](https://github.com/agentclientprotocol/claude-agent-acp/issues/1177))
([908c994](908c994901))


### Bug Fixes

* recreate live sessions on logout when it clears a provider or gateway
override
([#1278](https://github.com/agentclientprotocol/claude-agent-acp/issues/1278))
([966be7a](966be7abe6))
* report a failed turn with the error stop reason on the experimental
ACP v2 surface
([#1288](https://github.com/agentclientprotocol/claude-agent-acp/issues/1288))
([060bdb2](060bdb212a))
* report an interrupted tool call as cancelled on the experimental ACP
v2 surface
([#1289](https://github.com/agentclientprotocol/claude-agent-acp/issues/1289))
([55c3106](55c3106c60))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: acp-release-bot[bot] <246668977+acp-release-bot[bot]@users.noreply.github.com>
v0.89.0
2026-10-10 02:03:17 +04:00
nikita-ashihmin
03d84f6663 feat: add the AIR sessionIndex extension for fast session lists (#1268)
## Summary

Adds the opt-in AIR `sessionIndex` extension. With it, an AIR client can
show the latest sessions of a project quickly, with their state, and can
rename, archive and unarchive them over ACP. The full contract is in
`docs/air-extensions.md` ("Session index").

The extension follows ACP RFDs. The transport differs: it uses
`_meta.jetbrains.air` request options and row fields, `_session/*`
methods, and AIR capabilities that are not advertised under ACP v2. The
`archived` list filter also differs (see below).

- **Session list extensions**
(agentclientprotocol/agent-client-protocol#2323):
- `limit`, ordered by last user activity (`lastPromptAt ?? updatedAt`),
and `includeWorktrees`.
  - Row fields `lastPromptAt`, `model` and `forkedFrom`.
- `state`: `running`, `idle`, `requires_action`, and `error`, which the
RFD lacks.
  - `lastTurnEndedAt`.
- `cost`: the SDK's `total_cost_usd` for a session that runs on this
connection only. No cost is read from transcripts.
  - Live updates come from an AIR list subscription (below).
- **Session archive** (agentclientprotocol/agent-client-protocol#2161):
- `archived` is on every row, `session_info_update` is sent on a change,
and an unknown session is `-32002`.
- The `archived` list filter has three values: `"unarchived"` (the
default), `"archived"` (archived sessions only) and `"all"` (both, in
one order). #2161 has a boolean here. Its `false` is `"unarchived"`, its
`true` is `"all"`, and `"archived"` has no counterpart.
- **Capabilities**: an AIR client that declares `sessionIndex` gets
these in `_meta.jetbrains.air.capabilities`. The client declares only
`sessionIndex`.
  - `sessionIndex`.
- `sessionArchive`: `_session/archive` and `_session/unarchive`. The RFD
capability is `archive` (#2161).
- `sessionRename`: `_session/rename`. The RFD has capability `setTitle`
and method `session/set_title`
(agentclientprotocol/agent-client-protocol#1987).
- `sessionListSubscribe`: `_session/list/subscribe`,
`_session/list/unsubscribe` and `_session/list/changes`. No RFD
counterpart.

### Clients without `sessionIndex`

Clients that don't declare `sessionIndex`, which means non-AIR clients
and released AIR builds, keep the current behaviour. They get the same
SDK `listSessions` path, page size, cursor and initialize capabilities,
with no watchers and no notifications. There are two deliberate
deviations, both strictly safer:

1. **`session/delete` from an AIR client without `sessionIndex`.**
Released AIR builds use delete as "Done". Instead of deleting the
transcript (data loss), the adapter archives the session in AIR's own
Claude format (below). Archived sessions are hidden from that client's
list, so "Done" looks the same and nothing is lost. An unknown session
keeps the SDK's error. Other clients get the SDK delete, as before.
2. **`session/close`** of a session that is not loaded here returns `{}`
instead of throwing.

### Sessions open in another process

`session/load`, `session/resume` and `session/prompt` work on a session
that another live Claude Code process has open, for every client, as the
Claude CLI does. Rename, archive, unarchive and delete also go ahead,
writing records or deleting transcripts as the CLI's `/rename` and
delete do. The other process keeps running.

### Archive format and rule

The archive state uses AIR's native Claude archive format
(`ClaudeStoreThreadBackend.appendTitleRecords`, ADR 0140), so AIR's
native path and the adapter read and write the same state.

- **Archive** appends `{"type":"custom-title","customTitle":"[archived]
<title>",…}` and `{"type":"agent-name","agentName":"[archived]
<title>",…}` to every transcript copy. **Unarchive** appends the same
pair without the prefix. Titles are cut to the CLI's 200 characters.
- **Archived is decided by the agent name first**, as in AIR's native
Claude provider. A session's name is its last top-level `agentName`,
else the custom title the SDK reports. The session is archived exactly
when that name starts with `[archived] `. The listed title comes from
the same source, shown without the prefix.
- Title records are metadata: `updatedAt` and the list order do not
move.

### Rename, archive and delete

- **Rename** writes the same title pair. An archived session keeps its
prefix. A session running here is renamed through its CLI's
`rename_session` control request, serialised with auto-titling. Other
sessions get the records plus the `custom-title.json` sidecar the CLI
writes. No generated title replaces a client title, but a title stored
later by someone else (a `/rename`) is still adopted.
- **Archiving stops the running session.** `_session/archive` of a
session loaded on this connection does four things:
  1. It interrupts the running turn.
2. It retitles the session through its CLI. The adapter also appends the
matching `agent-name` record to the CLI's transcript, because the CLI
writes that record only later.
  3. It closes the session as `session/close` does.
4. It archives the transcripts once more if the closed CLI left them
unarchived.

The client then gets `session_info_update` with `archived: true`.
Unarchive does not reopen the session. A session that is not loaded here
is archived by writing the title records.
- **Delete** for a `sessionIndex` client deletes every copy of the
session and its `<sessionId>/` directories. A session running here is
closed first.

### Implementation (`src/session-index/`)

- **Index**:
- Reads the project dirs with `readdir` + `stat`, and keeps a 2000-entry
metadata LRU keyed by (path, mtime, size).
- Cache misses are read in parallel through `getSessionInfo`, plus a
growing tail read (64 KB → 4 MB) for the last prompt, last message, turn
end and agent name.
- SDK semantics are kept: sidecar titles, `continued-in`, long-path
hashes, darwin case, and first-prompt extraction (ported, and
fuzz-compared with the SDK).
- The `cwd` scope follows the SDK `listSessions`. Worktrees expand to
the same relative subdirectory, and a session copied to several project
directories is listed from its newest copy.
- The keyset cursor is bound to `cwd`, `archived` and
`includeWorktrees`. Session ids match exactly.
- **State**:
  - For sessions of this process, the SDK state.
- For other sessions, the Claude Code live registry
(`~/.claude/sessions/<pid>.json`, with pid + procStart liveness and
Claude Desktop's rules) or the transcript tail. The registry is read
only for the row state.
- `error` means the session is not running and its last turn ended with
an error, not a cancel.

Documented in `docs/air-extensions.md`, with an extension → RFD mapping
table and the remaining differences.

### Measured on a real `~/.claude` (233 transcripts, read-only)

| Case | New path | Old SDK path |
|---|---|---|
| cwd, limit 50, cold process | ~80 ms | 78–156 ms (all rows) |
| cwd, limit 50, warm | 1.3–4 ms | — |

## Session list subscription

Replaces polling the first page. It is only for a client that declared
`sessionIndex`; the agent advertises `sessionListSubscribe` with it,
never under ACP v2.

- **Subscribe**: `_session/list/subscribe { cwd }` returns `{
subscriptionId }`. `cwd` must be absolute, else `-32602`; other params
are ignored.
- The scope is `cwd` and the same subdirectory in every linked worktree,
in any archive state, without sidechains.
- **Changes**: `_session/list/changes { subscriptionId, sessions,
removed }`.
- `sessions` holds the full `session/list` row of each session that
appeared, or whose title, `lastPromptAt`, `state`, `lastTurnEndedAt`,
`cost`, `model`, `forkedFrom` or `archived` changed since the last row
sent.
- The first change of a session after subscribe is sent in full. After
that, a change of `updatedAt` alone sends nothing.
  - `removed` lists sessions that were deleted or moved out of scope.
  - Archive and unarchive are row changes.
- A session is sent at most once a second, with one notification per
subscription per pass.
- **Unsubscribe**: `_session/list/unsubscribe { subscriptionId }`
returns `{}` and is idempotent.
- A subscription lives until unsubscribe or until the connection closes.
- A connection has at most 128 subscriptions. The 129th is `-32602` with
`data.reason: "too_many_subscriptions"`.
- Subscriptions of the same `cwd` share one watch and are notified
independently.
- **Delivery** is best effort, with no resync and no sequence numbers. A
rescan every 10 s (and 1 s after a project directory watch opens) sends
whatever differs.
- **Implementation** (`list-subscriptions.ts`, `session-info.ts`):
- A non-recursive `fs.watch` on the scope's project dirs, the registry
and the projects root.
- A transcript event re-reads only that transcript, and a registry event
reads only that `<pid>.json`. Sessions running here push their SDK state
at once.
- Subscribe reads no session row. The rescan reads only transcripts
whose stat changed or whose read failed.

### Measured: one subscription on the 5,200-session project (Docker
`node:22`, 2 CPU, 2 GB, shared host)

300 s of churn: 20 sessions appending 10/s each (updatedAt-only),
registry status flips every 200 ms, and a session created every 2 s and
deleted 10 s later.

| | Old `list_changed` watcher | Subscription |
|---|---|---|
| Agent CPU p50 / max | 19% / — | 12.9% / 49% |
| Event loop p99 (p50 / max of 1 s windows) | — | 16 / 31 ms; worst
single delay 87 ms |
| Change latency p50 / p95 | 950 ms (hint) | create 291 / 570 ms, delete
279 / 506 ms; registry state 902 / 990 ms* |
| Notifications / min | — | 90 (896 rows, 0 rows for updatedAt-only
appends) |
| RSS | flat ~160 MB | 159 → 198 MB (heap flat ~50 MB) |

\* The registry flips the same 5 sessions every 200 ms, so each is held
to the once-a-second limit. Latency is measured from the first unsent
change.

These numbers were measured with an earlier subscribe that read every
row up front. Subscribe now reads no rows, so that one-off cost no
longer occurs. The lazy subscription has not been re-measured yet.

## Load test: 10,000 sessions (Docker, 2 CPU, 2 GB)

Calibrated data: 10k transcripts, 15 GB, 36k subagent files, and one
5,200-session project. Sizes match a real profile (p50 311K / p90 2.67M
/ p99 20.5M).

| Scenario (limit 50) | Before | After |
|---|---|---|
| Big project, first page cold / warm p50 | out of memory | 225 / 19 ms
|
| No cwd, cold / warm p50 | 1,722 / 178 ms | 209 / 45 ms |
| Page through 4,637 rows | 457 s | 11.4 s cold, 2.9 s warm |
| Delete p50 / 100 concurrent | 23.8 ms / 4.4 s | 7.8 ms / 0.8 s |
| 300 s of churn (60k changes) | out of memory after 3 s | list p50 54
ms, 0 stalls >50 ms, RSS flat ~160 MB |

## Streaming: keep the event loop responsive under a backlog

Under a backlog of buffered SDK messages, the adapter could freeze its
event loop for seconds. Every `query.next()` on a non-empty SDK queue
resolves in a microtask, so `runConsumer` forwarded the whole backlog
without reaching a macrotask boundary. Incoming JSON-RPC, including
`session/cancel`, waited until the backlog drained.

1. **Consumer yields to the event loop.** A shared `EventLoopYielder`
(`src/utils.ts`) gives the event loop a turn via `setImmediate` once the
current iteration has run for 8 ms. Order, content and cancel semantics
are unchanged.
2. **No per-message copy in `nodeToWebWritable`** for byte streams.
Object-mode streams keep the copy.
3. **Diff memo.** The approval preview and the PostToolUse patch no
longer diff the same texts twice. Timed-out diffs are not remembered.
4. **Prefix guards** in `parseMcpCommand` and `sanitizeTitle`. Both give
identical output.
5. **Replay yields between history messages**, with the same budget.

With 10 sessions × 1 MB, the max event-loop delay went from 4205 ms to
1626 ms, and ping p95 from 2793 ms to 262 ms (interleaved A/B, Docker
`node:22`, 2 CPU). Single-stream throughput is unchanged. Remaining
stalls are inside the Claude SDK and GC.

## Test plan

- [x] Rebased on `origin/main`. `npm run check`, `npm run build` and
`npm run test:run` pass.
- [x] Regression tests for the unchanged non-`sessionIndex` paths.
- [x] Regression tests for every review finding.
- [x] Reviews: Codex (gpt-6-astra) and a Claude review subagent, run as
independent passes against `origin/main` until a round found nothing of
normal or higher severity.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
v0.88.1-preview.7
2026-10-10 01:59:47 +04:00
Ben Brandt
15034659f7 refactor: list a session's commands in its setup responses on the experimental ACP v2 surface (#1290)
The v2 draft lets the `session/new`, `session/resume`, and
`session/fork` responses carry `availableCommands`
([agent-client-protocol#2259](https://github.com/agentclientprotocol/agent-client-protocol/pull/2259)),
so a client can show a session's commands without waiting for an update.
The v2 surface now fills it.

## Before and after, for a v2 client

- **Before:** the setup response, then an `available_commands_update`
with the list, from a `setTimeout` after the response.
- **Now:** the list is in the response itself, for `session/new`,
`session/resume` with and without replay, and `session/fork`. The field
is left out when the list is empty, as the draft asks.
- **No duplicate:** on a v2 connection the agent no longer sends that
post-setup update (`afterSetupResponse`). Later changes still arrive as
`available_commands_update`, for example the terminal-bound filtering
after Claude Code's first init frame, or `commands_changed`.
- v1 and AIR are unchanged.

## Cost

None. The commands come from `Query.supportedCommands()`, which in the
SDK is `this.latestCommands ?? (await this.initialization).commands`:
the cached initialization result that setup already awaited. So the
response doesn't wait any longer.

## Code

- `ClaudeAcpAgent.availableCommands(sessionId)` returns the session's
command list, factored out of `sendAvailableCommandsUpdate`.
- The three post-setup `setTimeout` blocks become one
`afterSetupResponse`, which skips the command update on v2.
- `setupCommands` in `src/v2/agent.ts` adds the list in v2 form. A
failure to read the commands is logged and leaves them out, since
they're optional and the session already exists.

## Title

`refactor:`, like the earlier v2 surface additions (#1253, #1255,
#1256), so no release note announces v2 yet. Retitle it if you'd rather.

## Checks

- `npm run check`, `npm run build` and all 2494 tests pass.
- The 40 v2 golden files each move the list from the post-setup
notification into the setup response, and nothing else changes; I
checked every diff with a script. No v1 or AIR golden file changes.
- New v2 tests cover each setup response (new, resume, resume with
replay, fork) and check that no post-setup update follows.
- I removed each part in turn, and each removal fails tests: the agent
still sending the update on v2, and the commands missing from new, from
resume, or from fork.
- Not covered: the empty-list case, because the adapter always adds its
`/mcp` command.
v0.88.1-preview.6
2026-10-09 21:39:33 +02:00