mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 04:30:59 +00:00
0ce42f2c312d190f78024d8b4b1ae5f00d9e5939
20 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0ce42f2c31 |
feat: add batch session metadata lookup (#1408)
A session browser may load only the 50 most recent conversations, while pinned sessions and saved folders refer to much older ones. After a restart, the client still has those IDs but needs their current titles, locations and archive state to rebuild the saved views. Finding a handful of retained sessions by paging through the entire history becomes increasingly expensive as that history grows. Add `_session/batch_get` to retrieve metadata for known session IDs in one request; the client chooses how many IDs to send per request. Successful results use the same row format as `session/list`, including archived sessions and sessions in worktrees, so the client can merge them with its recent page and apply its usual display filters. The response contains the sessions whose metadata is available, in request order. Other IDs are simply omitted. The client can refresh the rows it receives while keeping its saved references, so pinned and folder views stay independent of the recent-list window. --------- Co-authored-by: Aleksandr Slapoguzov <aleksandr.slapoguzov@jetbrains.com> |
||
|
|
de96cef382 |
feat: add ultracode session config option (#1438)
Adds an **Ultracode** session config option. It turns on Claude Code's
dynamic workflows for every task, at any effort level. Clients that
support boolean config options get a native switch; the rest get an
On/Off select. This follows the Fast mode pattern.
## How the CLI handles ultracode (2.1.310, checked against the bundled
binary and a live session)
- **Which models:** ultracode works on exactly the models that support
`xhigh` effort. The CLI uses the same check for both. So Opus 4.7+,
Sonnet 5+, Haiku 5.5 and Fable qualify; Opus 4.6 and Haiku 4.5 don't.
Effort level no longer matters.
- **Workflows:** dynamic workflows must be enabled. They are off by
default on Pro plans, and org or managed policy can turn them off.
- **State reporting:** `query.getSettings()` returns
`applied.ultracodeAvailable` and `applied.ultracodeRequested`. The CLI's
schema says hosts should "offer an Ultracode control only while this is
true". This method exists at runtime but is missing from the SDK's
`.d.ts`, so the adapter reads it with a cast and falls back to settings
and model data when it is absent.
- **Unsupported requests:** `applyFlagSettings({ultracode})` is never
validated. The request is stored and stays in place across model
switches; it takes effect whenever the model supports it.
- **Effort changes:** an `effortLevel` change sent without an
`ultracode` key turns ultracode off.
## Behavior
- **Visibility:** at session start, one `getSettings()` call decides
availability and the starting value. Model switches combine the
workflows result learned at start with the new model's
`supportedEffortLevels`, so the switch path makes no extra control
request.
- **Value:** the session tracks the user's choice, like
`fastModeEnabled`. The option hides on unsupported models and comes back
with the same value.
- **Effort applies:** `effortFlagSettings` now carries the session's
toggle instead of `settings.ultracode`. This applies at the initial
apply, on model switch, and from the effort picker, so effort changes no
longer turn ultracode off.
- **Clear context:** the clear-context restart restores the toggle, as
it does Fast mode.
## Known limitation
If a user types `/effort ultracode on` as a prompt, the toggle does not
update. The adapter already doesn't pick up effort changes typed as
`/effort` commands.
## Testing
- New `src/tests/ultracode-config.test.ts`. It covers the helpers and
session behavior: seeding from the CLI's report, hiding while workflows
are off, following the model's support, keeping the value across effort
and model switches, and rejected applies.
- New clear-context restore test. `effortFlagSettings` tests updated.
- Live probe against CLI 2.1.310 confirmed the `getSettings` fields, the
`xhigh` proxy (`claude-opus-4-6` reports unavailable while the request
stays in place), and the effort/key behavior.
- `npm run check`, `npm run build`, `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>
|
||
|
|
9b3a5c891c |
feat: end a turn at its result for AIR clients with backgroundSubagents (#1262)
## What This PR adds the AIR client capability `backgroundSubagents`. A client declares it in `initialize.clientCapabilities._meta.jetbrains.air.capabilities`. With the capability, an ACP v1 prompt turn ends at its result. This is also true while background subagents of the turn still run, including a subagent that SendMessage resumed. Subagent updates, permission requests and elicitations then go out after the prompt response. Their routing does not change. The agent advertises the capability only to ACP v1 AIR clients. ## Why The hold of #864/#866 keeps `session/prompt` open until the subagents finish. The Claude CLI ends the model turn at `result`, and only `idle` waits for the background agents. In AIR the hold hides the end of the answer, and the next user message steers into an old turn. One SendMessage-resumed subagent kept a turn open for about two hours. ## Unchanged - A client without the capability keeps the hold exactly. So #866 stays fixed for clients that stop listening at the prompt response. - ACP v2 keeps the hold too. ## Related changes - Out of a turn, the ExitPlanMode request of such a client does not offer the clear-context option. The fresh query must continue an ACP turn, and there is none. - An ExitPlanMode interruption of an autonomous cycle with no held turn now clears its markers. ## Docs `docs/air-extensions.md` has a new "Background subagents" section. ## Tests New capability cases: - a fresh subagent; - a SendMessage-resumed subagent; - a permission request after the prompt response, with the `parentToolUseId` attribution; - an autonomous followup that does not end the queued next prompt; - ExitPlanMode out of a turn, with and without a reset; - the exclusion of a running subagent from the incomplete-tool check. The existing tests cover the path without the capability. When the gate is disabled, 7 of the 8 new cases fail. The eighth is the in-turn control case and passes in both configurations. `npm run build`, `npm run check` and `npm run test:run` pass. The exception is 4 `providers.test.ts` cases. They depend on the environment (a local proxy `baseUrl`) and fail on `main` too. |
||
|
|
e9f986b8a9 |
fix: IJAI-3744 keep a session running while its background tasks are open (#1399)
## Summary A Claude Code session can start a background shell command or a background sub-agent, and then end its turn. When a task ends, Claude Code wakes the session for a short turn. The session index reported `idle` at each turn end, so a client saw one completion for each task. Now the index finds the open background tasks in the transcript. A turn end with an open task stays `running`, also after 10 minutes. The session gets one `idle`, when the last task ends. The rules are the same as in the native Claude provider of AIR (IJAI-3744). ## Rules - A task opens with a tool result that has `backgroundTaskId`, or `isAsync` with an `agentId`. - A task closes with its `<task-notification>` (a user record or a queue `remove`), or with a `TaskStop` result. - The wait ends when no live process holds the session, or when the registry status is `idle`. - A task can start before the tail window. A later scan applies the tail on top of the earlier task set. When it cannot, the index reads the whole transcript, but only for a finished turn of a live session. ## Not in this PR - A background task that a sub-agent starts. Its start record is only in the transcript of the sub-agent. - A session that this connection runs. Its state comes from the SDK. ## Tests - `background-tasks.test.ts`: the record orders of the native provider tests, in a small transcript and in one longer than the tail window. The tests also cover a task before the tail window, the end of the wait, and the count of whole-file reads. - A real check with Claude Code 2.1.306 in a terminal. With two background shells or two background sub-agents, `main` gave three `idle` and this change gave one. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a1d0eeeaad |
fix: publish turns that Claude Code starts after end_turn as an async task in v1 (#1390)
## Motivation
Claude Code can start a turn of its own after a prompt has ended, for
example to answer the notification of a finished background task. ACP v1
has no session state, and the client already got `end_turn` for its last
prompt. The output of that turn just keeps arriving as session updates,
and the client has no signal that the agent is working again, or when it
stops.
## Change
The adapter now publishes such a turn as an async task (AIR async tasks
extension) with the id `autonomous-turn-<uuid>`, the name `Claude is
working`, `taskType: "turn"` and `showInTranscript: false`. Its
description is the summary of the last task notification, when there is
one.
- **Start:** the SDK moves to `running` while no prompt is active or
queued. From CLI 2.1.270 the state stays `running` while background
agents run, so the task also starts at the first root output with no
prompt open.
- **End:** the task ends `completed` at the turn's result or at `idle`,
and `failed` at an error result. A coalesced placeholder result
(`num_turns: 0`) does not end it.
- **Folded prompt:** a prompt that the turn takes in ends the task,
because that prompt owns the turn now.
- **Stop:** `_session/async_task/stop` on this task interrupts the turn,
since the SDK has no task to stop. It returns `{ "stopped": false }`
while a prompt waits for its turn, because the interrupt would drop that
prompt.
- **Lifecycle:** the SDK task list does not end the task, because the
SDK does not know about it. Shutdown fails it like other open tasks.
Clients without the async tasks capability get nothing new. The behavior
is documented in `docs/air-extensions.md` under "Autonomous turn".
## Tests
`acp-agent.test.ts` covers the start at `running` and at the first
output, the end at the result, the folded prompt, stop as an interrupt,
and a client without async tasks. `async-tasks.test.ts` covers the SDK
task list reconciliation and shutdown.
|
||
|
|
1420994ced |
feat: send one end event after the session/load history replay for AIR (#1361)
## What
A client that declares the AIR capability `loadReplayEnd` gets one extra
`session/update` after the history replay of `session/load`:
```json
{
"sessionId": "<loaded session>",
"update": {
"sessionUpdate": "session_info_update",
"_meta": { "jetbrains": { "air": { "version": 1, "loadReplayEnd": true } } }
}
}
```
The client declares the capability in
`clientCapabilities._meta.jetbrains.air.capabilities`, as it declares
the other AIR extensions. The agent does not advertise it.
## Why
ACP has no signal that the history replay of `session/load` is over,
other than the response. This adapter replays the transcript while the
Claude Code CLI starts, and the CLI start takes most of the load time.
So the history is whole long before the response. With this event, the
client can show the history at once.
The protocol side is the RFD
https://github.com/agentclientprotocol/agent-client-protocol/pull/2407.
## Design change
The first version of this PR marked each replayed notification with an
index (`loadSessionReplayIndex`) and the last one with
`loadSessionEndOfRecordedHistory`. To know the last one, it held the
latest update and queued every other outbound message of the session
behind it. That design is replaced with a capability-gated single end
event. The replay sends as before. Only one event is added at its end.
There are no indexes, no held update and no queue.
## When the event is sent
- After every replayed update, the subagent history that the parent
replay sends to the child sessions included, and before the
`session/load` response.
- Also for an empty history.
- For `session/load` of a session that starts while the replay runs, for
`session/load` of a session that is already live, and for the ACP v2
`session/resume` with `replayFrom: {type: "start"}`.
It is not sent:
- When the replay stops (the CLI start failed during the replay) or
fails.
- For a live update.
- To a client without the capability.
The update has no `title` and no `updatedAt`. A `null` there would clear
the value. The load can still fail after the event, for example when the
CLI start fails after the replay. The event means that the recorded
history arrived whole.
## Client status
JetBrains Air shows the history before the load answer only after the
Kotlin SDK delivers the updates of a loading session before the answer.
An SDK PR for this is in progress.
## Tests
- `create-session-options.test.ts`: the event comes once after the
replay and before the response; before the CLI start finishes; for an
empty history; for an already live session; not when the CLI start fails
part way; not when the replay fails; not without the capability. The
event has no `title` and no `updatedAt` key.
- `acp-agent.test.ts`: the subagent history from its own transcript file
comes before the event.
- `acp-v2.test.ts`: v2 `session/resume` with a replay ends with the
event before the response; a resume without a replay sends none.
- `docs/air-extensions.md`: a new "Load replay end" section, and the
capability and key rows.
|
||
|
|
872e93f11f |
feat: add the MCP server and tool to AIR tool calls (#1359)
AIR gets `_meta.jetbrains.air.mcp = {server, tool}` on MCP tool calls,
so the MCP card does not depend on the title format.
The adapter splits the `mcp__<server>__<tool>` tool name the way Claude
Code does. `server` is the normalized name. Clients other than AIR get
nothing new.
Split from IJAI-872. The SDK does not expose MCP progress yet, so this
PR does not forward it.
|
||
|
|
78a29f58ba |
feat: publish the predicted follow-up prompt as _meta.promptSuggestions (#1356)
## What
The Claude Code CLI can predict the prompt the user is likely to send
next. This adapter asked for nothing and dropped the `prompt_suggestion`
message. It now asks for the prediction (`promptSuggestions: true`) and
forwards the one prediction of a turn.
ACP has no field for a composer prefill, so the value travels as
```json
{
"sessionUpdate": "session_info_update",
"_meta": { "promptSuggestions": { "suggestions": [{ "text": "Run the tests" }] } }
}
```
The key names no vendor, so another client can read it. AIR already
renders the first non-blank text as gray text in an empty composer,
where Tab fills the composer and sends nothing.
## Behavior
- `suggestions: []` clears the prediction the client holds: at the next
`session/prompt`, because that turn's prediction is spent, and on a
`conversation_reset`, because the transcript it was drawn from is gone.
- A prediction the next prompt outran — the user sent the next prompt
before the adapter read the prediction off the SDK stream — is dropped
instead of published mid-turn.
- A blank prediction never goes on the wire.
- The update is sent only to an AIR client, like the other extension
keys, so non-AIR traffic is unchanged (the key is added to
`AIR_ONLY_META_KEYS`; the `origin/main` scenario baselines stay
byte-identical).
- The CLI predicts nothing on the first turn, after an API error, in
plan mode, and at the usage limit, so a turn without a prediction is
normal. A caller can opt out with
`_meta.claudeCode.options.promptSuggestions: false`.
Documented in `docs/air-extensions.md`.
## Tests
`src/tests/prompt-suggestions.test.ts` covers the wire shape, the
publish after the turn's result, the clear at the next prompt, the
dropped stale prediction, the blank prediction, and a non-AIR client.
`src/tests/create-session-options.test.ts` covers the SDK option and the
caller opt-out.
`npm run check`, `npm run build` and the touched suites are green; the
full `test:run` has pre-existing flakiness on this machine
(session-index watchers, providers) unrelated to this change.
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
|
||
|
|
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> |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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>
|
||
|
|
0e83a06c4a |
fix: async task routing, Monitor tasks, structured task ids, and abandoned tool calls (#1249)
It affects AIR clients with the `asyncTasks` capability.
## Problem
- A subagent can start a background Bash or Monitor and then end its
turn. The task ends after the subagent finished. The route of the task
dropped every update of a finished child generation, so the client never
got the terminal `async_task_state_update`. The task stayed running in
the client forever. One AIR log held 440 such dropped updates.
- The `Monitor` tool streams the stdout of a background command to the
model only. The SDK reports its task as `local_bash`, so the client
showed it as a background shell. In one real session, 18 of 18 Monitor
tasks had no output file, no progress and no result. The Stop button
only broke the agent's wait.
- The adapter read a task id from the text of a Bash tool result. A
foreground `sed`, `strings` or `python3` that printed adapter source,
test fixtures or CLI strings became a background task, for example with
the id `";\n const start = text` or `${e}`. The client got a spawn and a
`backgrounded` marker on a foreground call, and the task never ended.
- Claude Code 2.1.287 reports a background command in four texts, and
only one gave an output path. A subagent tool result almost never has
`tool_use_result` (0 of 48 in one session). So a subagent command that
hit its timeout got no output path (9 of 10).
## Fix
- `NativeSubagentRuntime.routeOfToolCall` still sends
`async_task_spawned`, `async_task_progress` and
`async_task_state_update` to the child generation that owned the tool
call after that child finished. It never sends them to a later
generation. Every other late update is dropped as before. The spawn is
in the list because a held task can get its tool call id after the
subagent finished.
- `AsyncTaskRuntime` takes a `toolNameOf` lookup. A task of a `Monitor`
tool call is ignored, like a subagent task: no spawn, no progress, no
state, and no stop. A held task that gets its tool call id later goes
through the same check. A `local_monitor` task is ignored too. The
`Monitor` tool call stays in the transcript.
- A task comes only from structured data:
`tool_use_result.backgroundTaskId` or the SDK task events. The text id
parser is gone.
- The output path of a known task can come from the result of its own
tool call. The path ends with `tasks/<id>.output` and starts after the
nearest `": "` on the same line, so a path with spaces works, for
example under `C:\Users\John Doe\AppData\Local\Temp`. It must be
absolute (POSIX, a Windows drive or UNC), and it never holds the end of
another path. The parse is linear in the text length. All four texts
give the path in both event orders: the tool result before or after the
SDK names the task. The memory is bounded. A structured path still wins,
and the terminal `output_file` reaches the client also after a level
close.
- The `backgrounded` marker comes only from structured data: the
structured task, a known background task of the tool call, or a
`run_in_background` input. The "both event orders" above holds for the
path, not for this marker. The marker rides the tool result update, so a
command that the SDK names only after its tool result gets the marker
only from a `run_in_background` input. A command that the user moved to
the background by hand gets no marker in that order.
- The replay behaves the same in each case.
## Tests
- Native subagent and agent tests: a task update and a late spawn after
the child finished go to `worker-1`; a held task of a subagent gets its
spawn and state in the child session after the subagent finished; a late
message chunk is still dropped; a subagent's background Bash ends after
the subagent and the client gets the terminal state in the child
session.
- Monitor: runtime, live and replay tests, also a task that names its
tool call after the tool result. A Monitor task cannot be stopped.
- Structured ids: foreground output with each marker text creates no
task; each of the four texts gives the path in both orders, also a
Windows and a POSIX path with spaces and a UNC path; no path of another
task, another tool call, `.outputs`, a relative path, a path without a
`": "` boundary, or a path across a line; two paths on one line are not
glued; a 2 MB text without `": "` parses in milliseconds; a structured
path wins. An agent test covers a subagent without `tool_use_result`,
and a replay test takes the path from the tool result.
- The new tests fail without the change. Build, lint, format and all
tests pass.
## Abandoned streamed tool calls
This part affects every client, Zed included.
**Problem.** The adapter opens a tool call at the `content_block_start`
of a streamed `tool_use`. Claude can abandon that block before it is
complete, for example for a steering message. The tool then never runs.
At end_turn the settle check reported the session failure "Claude ended
the turn without returning results for tool calls"
(`incomplete_tool_call`). A real AIR session showed a red service error
after 27 minutes of normal work, for a call that never had input.
**Fix.**
- The session records the tool calls that Claude Code sent to the tool
runner: the `tool_use` blocks of a complete assistant message, and the
tool calls of a permission request. The record is bounded and is dropped
when the call ends.
- At end_turn, an open foreground call without that record is abandoned.
The adapter closes it as failed with "Claude stopped this tool call
before it ran." and reports no session failure. The turn ends as
`end_turn`.
- An open call with that record keeps the old behaviour: the old failure
text and the `incomplete_tool_call` session failure.
- The cleanup and the cancellation checks are the same for both groups.
- The replay does not change, because the history holds no partial
`tool_use`. No scenario golden file changes, for AIR, plain or Zed.
**Tests.** A partial `tool_use` and then text ends as `end_turn` with
the neutral close. A mixed turn still fails for the unfinished call. A
steering message during a partial `tool_use` gives no failure. The
existing incomplete-tool tests now send the complete `tool_use` and keep
their failure. The new tests fail without the change.
|
||
|
|
190a00f68f |
feat: answer /mcp in chat with the MCP server status and a reconnect (#1218)
## Summary In an ACP client, `/mcp` showed the CLI text "… Use `/mcp` in the terminal for details." The user of an ACP client has no terminal. In SDK mode, Claude Code also refuses `/mcp reconnect|enable|disable` with "Reconnect, enable, and disable aren't available in this session." and changes nothing. - `/mcp` and `/mcp reconnect|enable|disable [<server>|all]` still run as a normal Claude Code turn. The adapter replaces the text of the local command, through the same hook that `/usage` uses. Prompt order, cancel, and the provider update stay with the turn. - For an action, the adapter runs it in that turn through the SDK control API (`reconnectMcpServer`, `toggleMcpServer`). It shows one result line for each server, then the status list. - The status list comes from `query.mcpServerStatus()`. It has a summary line and groups by status: failed, needs authentication, connecting, connected, and disabled on one line. A failed server shows a short cleaned error. A list is used and not a table, because a table breaks words in narrow columns. - Useful CLI sentences stay, for example "There's no MCP server named …". Sentences that point to the terminal are removed. - After an explicit reconnect, a server from the ACP session that needs authentication starts the existing MCP OAuth flow, when the client supports URL elicitation. - `/mcp` is advertised with the input hint `[reconnect|enable|disable [<server>|all]]` and is hidden on history replay. ## Other commits in this branch This branch also carries commits of other work: skill command metadata for AIR, a faster `session/load` replay, and the resumed permission mode. They can move to separate PRs if preferred. ## Tests - `src/tests/mcp-command.test.ts` (61 tests). The fake CLI answers with the real SDK-mode texts and message shapes (synthetic assistant + result) and does not change any status. - A live run through the adapter with a broken and a working stdio server: `/mcp`, `/mcp disable good`, `/mcp enable good`, `/mcp reconnect broken`, `/mcp reconnect nosuch`. - `compare.ts` allows one new difference against the origin/main baseline: the `mcp` command in `available_commands_update`. The baseline files are unchanged. - `npm run check` and `npm run test:run` pass. |
||
|
|
af0b43de94 |
fix: send the legacy custom answer key to AIR (#1210)
Released AIR versions, such as 262.air/release-09, read the custom answer companion only from the root key _meta._askUserQuestionCustomAnswer. The adapter sent only _meta.jetbrains.air.customAnswer, so these versions showed the companion as a separate question. For an AIR client the companion now carries both keys with the same value. Other clients get no key, as before. Remove the root key when the ACP registry no longer serves AIR versions that read only that key. IJAI-2892 |
||
|
|
875d75f2a4 |
feat: AIR tool call contract, exact git patches, plan file, and fixes for every client (#1153)
## Summary One PR with separate commits (the earlier stack #1168 and #1169 is folded back in). ### Exact git patches - `src/diff.ts` builds exact git patches for Edit and Write: real line numbers, file modes, CRLF, the final-newline marker, a size limit (including a bound for `replace_all`), binary detection, and an asynchronous diff with a time budget. The approval preview mirrors the input normalisation of the bundled CLI. - No creation patch over an existing file or for an Edit of an existing empty file, and no patch for a Write that changed nothing. ### For every client Clients that do not declare `_meta.jetbrains.air` (Zed, plain ACP clients) get the same information as on `main`. - A `tool_call_update` omits top-level fields that did not change since the last report of the same tool call (ACP merge semantics). The `_meta` of each report stays complete. - Bug fixes: a failed approval preview no longer blanks the approval diff; a permission denial replaces a pinned approval patch; the compaction summary is not repeated when the streamed text is identical; streamed subagent text is not sent twice; async task fields are recorded only after a successful send; late updates of a finished native subagent are dropped; the updates of a fresh Claude context after a clear-context restart reach the ACP session. ### For AIR only - One fact in one field: input in `rawInput`, the file text of an edit only in the diff (`diffPatch`), output once, no display copies of the input (`rawInputRendering`), `_meta.claudeCode` reduced to `toolName`, `parentToolUseId` and the `toolResponse` markers, per-key `_meta` filtering. - Plan file mode (`planFile`): ExitPlanMode carries only `rawInput.planFilePath`; AIR follows the file that Claude writes. - AIR-only keys move under `_meta.jetbrains.air.*` and are no longer sent to other clients. ### Structure and docs - `src/tool-calls/`: one reporter per tool, one `ClientCapabilities`, one `AcpToolCallRenderer`, the field tracker and the changed-meta filter. - `docs/air-extensions.md` replaces the per-extension AIR docs. ## Tests - `npm run build`, `npm run check`, `npm run test:run`: 1995 passed. - Scenario tests for plain, Zed, and AIR clients: the SDK mock honours the adapter's session id, every update must reach its ACP session, every message is validated against the ACP JSON schema with all formats checked, plain and Zed are compared with a recorded `main` baseline, AIR keeps compact golden recordings. |