Both cores call session-env's configuredSettings, which now applies the
models of CLAUDE_MODEL_CONFIG itself. The v2 wrapper of the same name is
gone.
Co-Authored-By: Claude <noreply@anthropic.com>
resultEnd now makes exactly the decisions of v1's subtype switch, and the
v1 prompt loop calls it, keeping its own session failure categories and
result forwarding. v1 is unchanged. v2 now follows v1 where the copies
differed: a limit result (error_max_turns, error_max_budget_usd,
error_max_structured_output_retries) ignores stop_reason max_tokens, and a
success error with the /login text fails with authRequired even at
max_tokens. isLoginRequiredText moves to result-end.ts.
Co-Authored-By: Claude <noreply@anthropic.com>
A route change (providers/set, providers/disable, an auth/logout that
drops a provider or gateway login, or a resume with new additional
directories) now restarts a session at a turn boundary right away, and
the request waits only for those restarts. A busy session (an open turn,
a pending prompt, a live subagent, or a restart that runs) restarts at
its next turn boundary without the request waiting, and its prompts keep
going to the current query until then. Prompts wait only for a restart
that runs.
Co-Authored-By: Claude <noreply@anthropic.com>
Describes the adapter's ACP v2 implementation as "the v2 agent" in
src/v2/, keeps the differences from and history of the old v2 bridge,
and points README.md, AGENTS.md and docs/air-extensions.md at the new
path.
Co-Authored-By: Claude <noreply@anthropic.com>
Updates `docs/v2-native-core.md` (status, a history note in place of
"Replacing the bridge", the differences from the old bridge in the past
tense, the test setup), the README, the AIR extensions doc and
AGENTS.md, and rewrites code comments that described the bridge.
Co-Authored-By: Claude <noreply@anthropic.com>
A v2 client that declares _meta.jetbrains.air now gets the native core,
which serves it the AIR extensions that ACP v2 does not cover, with the
v1 core's wire output:
- recommendedValue on the model and effort options, and the shown effort
set on the query at the start and at each model switch
- the AIR permission presentation and AIR's permission tool call meta
- AskUserQuestion's custom answer marker
- the predicted follow-up prompt, cleared at the next prompt and at a
conversation reset
- agentFileChangeReport, sent at the end of the turn that serves the
prompt, before its idle, since a v2 prompt is answered at insertion
initialize lists agentFileChangeReport and recommendedValue to an AIR
client only. The extensions that v2 covers (subagent sessions,
background lists, file plans, patches, compaction, systemPrompt, failed
idles) are not served. CLAUDE_AGENT_ACP_V2_CORE=bridge still serves AIR
with the bridge.
Co-Authored-By: Claude <noreply@anthropic.com>
A foreground subagent whose task_started reached the native v2 core after
the root's session/cancel, but before the cancelled work's result, kept
its permission and elicitation requests going to the client and its
consolidated messages going out. SessionTasks now keeps the root's cancel
in effect until the stopped work's result, Claude Code's idle, the next
prompt's start, or a failed interrupt, and stops such a subagent the same
way as one present at the cancel. A request of a subagent whose run a
cancel stops is withdrawn before it goes out.
Co-Authored-By: Claude <noreply@anthropic.com>
- `resolveModelPreference` resolves `best` (and `best[1m]`, whose hint the
CLI ignores) as `opus`, the CLI's flagship fallback, unless an entry's
value is `best`. Its preference words drop leading gateway segments before
`claude-` (`vertex_ai/`, `openrouter/anthropic/`, `litellm/`,
`publishers/anthropic/models/`) and a trailing `-latest`, which resolved
before 51ab745 and gave null since.
- With `v2Core: "native"`, a v2 client that declares `_meta.jetbrains.air`
gets the bridge (`airClientsOnBridge` in serve.ts): the native core serves
no AIR extension, so a v2 AIR client would silently lose sessionFailure
(API retry warnings), prompt suggestions, file change reports and AIR's
subagent form. The docs' "Not served yet" section is corrected.
- `v2-native-integration.test.ts` (RUN_INTEGRATION_TESTS) runs the default
v2 core in the adapter's process against the bundled CLI: insertion,
running -> idle, requires_action at a permission request, and a cancel
that fails a queued prompt with -32800.
CLAUDE_AGENT_ACP_V2_CORE now defaults to native; CLAUDE_AGENT_ACP_V2_CORE=bridge
selects the bridge as a fallback. acpProtocolRouter's v2Core option defaults to
native too. The unit vitest project pins the v2 tests and scenarios to the
bridge, and the v2-native-core project runs them on the native core.
As the bridge does: after the root's session/cancel, a foreground child's
consolidated messages are dropped until its run ends, apart from the results
of the tool calls the client holds; a conversation_reset ends each working
child idle with no stop reason and forgets the children. Documents that both
cores reject a replayFrom cursor other than start with -32602, and the
remaining intended differences.
resolveModelPreference matched preference tokens as substrings, so
"not-a-model" resolved to Default ("a" is inside "default") and
"son" or "foo-sonnet" to Sonnet. configOptionValue, which v1, the v2
bridge and the native v2 core share, then accepted the value. Words now
match whole, and the fuzzy tiers need every model word of the
preference; provider ids, dates, -fast and context hints are dropped
with normalizeModelId first, so aliases and provider spellings still
resolve.
acp-scenarios-v2.test.ts runs again in the v2-native-core vitest project
with V2_CORE_UNDER_TEST=native, comparing every message of each run with
the bridge's golden files and never writing them. It supersedes
acp-scenarios-v2-native.test.ts, which compared only session updates.
- Restarts restore effort, Fast mode, ultracode, the picked agent and
prePlanMode as the bridge does (SessionConfig.restoreFrom), and start
without the setup's _meta effort (withoutEffort, moved to session-effort).
- A route restart waits for a running account restart; the old query ends
at its next turn boundary after the new one started; a close during a
restart forgets the session.
- Config changes of a session run one at a time.
- The subscription guard runs in the session's prompt chain: cancel and
close answer a prompt it judges with -32800, and /remote-control keeps
its place.
- systemPrompt is validated on every setup path before it acts (active
resume, fork before the copy).
- A session whose query ended stays active until close: its requests fail
with -32603 and what happened instead of -32002.
- A resume of the active session with other additional directories
restarts it with them at its next turn boundary.
- A replayed tool call without a result ends failed.
- The clear-context prompts get no user_message; the clear-context mode is
resolved at the answer, with no fallback notice.
- No "needs authentication" notice when the session runs MCP OAuth.
Port the bridge's effort, Fast mode, ultracode and agent options and their set
paths, the refusal fallback notice and model swap, the context window seed
(getContextUsage and the cross-session cache), conversation_reset handling, and
ExitPlanMode's clear-context option to the native v2 core.
The protocol-neutral code moves out of acp-agent.ts into shared modules that
both cores use: session-config-options.ts, session-fast-mode.ts,
context-window.ts, the effort switch helpers in session-effort.ts,
withCurrentModelInfo, and modelRefusalFallback.
The native core continues an accepted clear-context plan with Claude Code's own
/clear on the same query instead of the bridge's fresh query; the client sees
the same updates.
- providers/set, providers/disable and an auth/logout that drops a route
restart each live session's query at its next turn boundary, resuming the
same Claude session, as the bridge does (src/v2-native/restarts.ts).
Prompts sent meanwhile wait and go to the new query.
- The --hide-claude-auth subscription guard holds on the native core, as the
bridge applies it to v2 sessions (src/v2-native/subscription-guard.ts).
- /remote-control is listed and run as a local command of its own turn, and
messages typed in another client show as user messages
(src/v2-native/remote-control.ts). Its helpers move out of acp-agent.ts
into src/remote-control.ts.
- Docs: the gateway login routes only new sessions on the bridge too;
steering is not served to v2 by the bridge either; file change reports are
an AIR extension; the active-resume MCP server and /remote-control
differences are listed as intended.
A child's configuration from its hooks (mode, effort, agent type,
PostModelSwitch) merged with its response models, SendMessage and
SubagentHandback calls as session messages live and in a replay, a
child's tool progress, the replay's notice for missing subagent history,
and the restore of an agent from before a restart from the session's
subagent history, as the bridge does. The hook options and their order
move to subagent-configuration.ts, shared with the v1 core.
- share recordStreamedDelta and unstreamedRemainder (streamed-text.ts) with
the v1 core: a consolidated message adds the tail of a cut-short text block
- after session/cancel, drop the stopped work's consolidated messages except
the results of tool calls the client holds, as the bridge does
- report zero usage and quota on the idle of a prompt's turn without a result
- run acp-v2.test.ts against the native core too, as a vitest project
- End a child's unfinished tool calls before its idle (cancelled, failed,
or dropped before it ran), as v2 asks of updates after a cancel.
- Skip the late result of a dead turn (cancelled after started), and keep
dropping an abandoned prompt's run when another one's cancelled frame comes.
- Send no request of a session after session/close answered.
- Resume of the active session: announce every working child, send each
listed item of a child before its state, and keep live children's states
after a replay.
- Report running again when a cancel's stop or interrupt fails.
- Keep requires_action for an accepted MCP URL elicitation until Claude Code
reports it complete, and only when every foreground subagent is blocked.
- Read the idle's stop reason with the v1 core's rules (shared resultEnd),
map assistant error kinds and the sign-in message like the bridge, and use
model_refusal_no_fallback's explanation.
- Serve SandboxNetworkAccess tool calls, permission_denied, the subject's
_meta (shared permissionToolCallMeta), prePlanMode, the Auto mode fallback,
PostModelSwitch, MCP OAuth after fork, and the stopped-work check for
AskUserQuestion.
- Read the settings file before the settings watchers open (shared
configuredSettings), and forget canUseTool waiters that timed out.
Ports the session reports that the bridge sends to v2 clients to the native
core, in src/v2-native/reports.ts: usage_update (context window, cost, model,
rate limit), the _meta.quota of each idle, Claude Code's warnings and client MCP
startup failures as notices, live and replayed compaction updates, the idle
compaction (/compact as running then idle, with no user_message), titles,
Task* plans, tool progress and memory recalls.
Shared with v1 instead of copied: the usage helpers move from acp-agent.ts to
session-usage.ts, the informational and MCP startup notices and the replayed
compaction move to session-notices.ts and context-compaction.ts, and
SessionTitles reads its session and agent through interfaces that
ClaudeAcpAgent already matches.
session/cancel now interrupts Claude Code also when no turn is open, as the
bridge does, which stops a turn Claude Code started but did not report yet.
Announce a child once its task_started and its Agent call came, on the
session that made the call, with its state; replace the Agent call with the
linked session messages of its prompt and a foreground child's reply; stream
the child's traffic and report its model. Requests of a foreground subagent
block the work that waits on it also before its announcement, a stopped
child's tool calls end cancelled after its idle, and the background lists
follow Claude Code's live task set. Replay each subagent's history to its own
session on resume with replayFrom.
Co-Authored-By: Claude <noreply@anthropic.com>
- Idles carry the result's usage and `_meta.quota` (quota helpers moved from
acp-agent.ts to usage-quota.ts); a failed turn's idle carries its error only.
- A turn starts at a prompt's start or echo, or at the main conversation's
output; `system/init` no longer opens one. The idle that follows a result
is that result's, and a placeholder result ends nothing.
- session/cancel answers every prompt not inserted yet with -32800 and
always interrupts. The run of a prompt Claude Code had taken in, or of a
cycle it started while the client saw none, is dropped, and its tool uses
are denied (stopped-work.ts). An aborted result after a cancel ends as
cancelled.
- Permission requests stay open for Claude Code's interrupt to withdraw;
their end reports no running only when a cancel withdrew them.
- session/close ends the work at once, as the bridge does.
- Tool calls that Claude dropped before they ran end cancelled before the
idle; after a cancel every open call does, and tool results render as the
cancelled run's.
- Local command output and refusal explanations get their messageIds.
Advertise and serve session/fork, list the bridge's command shape (with
the adapter's /mcp, served) and send it in an update when reading it for
the response fails, follow _meta.claudeCode.options, MAX_THINKING_TOKENS,
CLAUDE_MODEL_CONFIG, the availableModels allowlist and the model
preference, restore the resumed transcript's mode and model, replay
compactions and approved plans, send the current title and AIR's
loadReplayEnd on resume, and show an Edit's exact patch before it runs.
AskUserQuestion as a form elicitation (no longer disallowed with form
support), MCP server elicitations (form and URL, cancel when the client
cannot render them), the refusal fallback's consent prompt, and MCP OAuth
after the setup response. They are open requests of their session
(requires_action), withdrawn by a cancel or close like permission requests.
The answer logic moves to shared helpers in elicitation.ts and the OAuth
flow to mcp-oauth.ts, which the v1 core now calls too.
Move the authStatus state and CLI probe (AuthStatusReporter, cliLogout) and the
provider routing (providers.ts) out of ClaudeAcpAgent, so the v1 core and the
native v2 core share them. The native core serves _meta.authStatus,
_auth/status_update, gateway auth/login, auth/logout and providers/* from
src/v2-native/auth.ts. A route change applies to sessions started after it;
unlike the bridge, live native sessions are not recreated.
Serves ACP v2 straight from the Claude Agent SDK query, without the
v1 core, behind CLAUDE_AGENT_ACP_V2_CORE=native. Foreground work is
each Claude Code turn, subagents are child sessions (subagents RFD),
and every state carries its background list. See
docs/v2-native-core.md.
Claude Code compacts a large idle session shortly before its one-hour
prompt cache expires, but only in attended sessions. SDK sessions
(entrypoint `sdk-ts`) never arm that timer, so every return after an
hour re-writes the whole context to the cache.
The adapter now runs its own timer: 54 minutes after the last
main-thread request, if that request used the 1h cache, the context is
at least 200k tokens and the session is idle, it sends `/compact`
through the normal prompt path. It respects `idleCompaction: false`,
`autoCompactEnabled: false` and `DISABLE_COMPACT` /
`DISABLE_AUTO_COMPACT`. AIR gets `trigger: "idle"` in the compaction
`_meta`.
Closes#1454Closes#1518
Brings back the standalone executables that #525 and #572 turned off.
npm publishing is unchanged. Each stable release also attaches 8
archives (Linux glibc and musl, macOS, Windows, on x64 and arm64) to its
GitHub release.
## Why it works now
The old executables ran the SDK's JS CLI from inside Bun's `$bunfs`, and
it couldn't reach its `vendor/` files there. The SDK's CLI is now a
native binary shipped in a per-platform npm package. So each archive
ships that binary as `claude` next to `claude-agent-acp`, and the agent
runs it from its own directory. Nothing is embedded or extracted, and
the agent has no `node_modules` to resolve.
The first commit embedded the CLI and extracted it with the SDK's
`@anthropic-ai/claude-agent-sdk/extract` helper. That works, but the OS
can only run a real file, and Bun-embedded files only exist inside the
agent process. So it wrote a second ~300 MB copy to `/tmp` on first run,
once per version. Shipping the CLI next to the executable avoids that,
and the download size is the same (~180 MB `.tar.gz`).
Claude Code's helper tools work too. Native Claude Code has no Grep or
Glob tools: its Bash shell wraps `grep` and `find` in functions that run
the ugrep and bfs built into the binary at `$CLAUDE_CODE_EXECPATH`. That
variable points at the `claude` shipped next to the agent.
## Changes
- `src/claude-cli.ts` now owns `claudeCliPath()`, moved out of
`acp-agent.ts`. `useBundledClaudeCli(dir)` makes it return
`<dir>/claude[.exe]`. If that file is missing, it throws an error saying
to keep `claude` next to `claude-agent-acp` or set
`CLAUDE_CODE_EXECUTABLE`; before, the user got a bare ENOENT.
`CLAUDE_CODE_EXECUTABLE` still wins.
- `src/binary-entry.ts` is the executable's entry. It registers the
directory of `realpath(process.execPath)`, so a symlink in
`/usr/local/bin` works, then imports the agent. It's left out of the npm
tarball.
- `scripts/build-binary.mjs` (`npm run build:binary`) runs `bun build
--compile` for a target and copies that target's `claude` next to the
output.
- `scripts/smoke-binary.mjs` (`npm run smoke:binary`) runs:
- `--version` and `--cli --version`
- a symlinked executable
- the missing-`claude` error
- `initialize` over v1 and v2
- the `terminal-auth` login command, run with `--version` (checks that
no `$bunfs` path leaks into it)
With `--full` it also drives a real session:
- Bash `grep`/`find`, checking that they ran Claude's embedded ugrep/bfs
- Read on text and on a PNG
- Edit
- a subagent
- `session/cancel` during a running command
- `session/list`
- `session/load` in a new process
- `session/fork` and `session/delete`
- `src/agent-command.ts` builds the `terminal-auth` login command, which
reruns the agent. Under Node that's `node <script> ...flags`. In a
compiled executable, `argv[1]` is a path inside the executable
(`/$bunfs/root/claude-agent-acp`), so it's dropped. Passing it back, as
the old `argv.slice(1)` did, handed it to the Claude CLI as an argument
and broke `auth login`.
- `.github/workflows/binaries.yml` builds and smoke-tests each target on
its own platform's runner; the musl targets are tested in an Alpine
container. CI runs it on every PR, using the basic smoke test, since CI
has no Claude login. On a stable release, `publish.yml` attaches the
archives (same names as before). These jobs don't gate the npm publish,
and previews get no executables. On stable releases the registry
dispatch waits for `upload-binaries`, so the archives (and the digests
the registry reads) exist first. It only waits: a failed upload doesn't
stop the dispatch.
## Testing
I also ran the whole unit suite on Bun's runtime (`bun --bun vitest
run`). 3,573 of 3,575 tests pass. The two failures are test artifacts:
Bun's `os.homedir()` ignores a `HOME` changed at runtime, and one
steer-and-cancel test asserts before the mock delivers its last idle. An
idle-accounting trace is identical on both runtimes.
On macOS arm64 with Bun 1.4.4, `smoke:binary --full` passes all checks.
`CLAUDE_CODE_EXECPATH` pointed at the shipped `claude`, and nothing was
written to the temp directory. The basic smoke test also passes from an
unpacked archive outside the repo with `PATH=/usr/bin:/bin`, and `node
dist/index.js` passes the full run. On Linux, Windows and Intel macOS,
only the basic smoke test runs, in this PR's CI.
Request validation from the ACP schema v1.28.0 / v2.0.0-alpha.11 review.
Each change has a test.
**Paths**
- `session/list` rejects a relative `cwd` with -32602. Without a session
index it passes `includeWorktrees: false`, so the filter matches only
sessions of exactly that directory, not other git worktrees of the same
repo.
- Resume, load and fork check `cwd` and `additionalDirectories` before
anything else. Before, a resume with a bad `cwd` tore down the live
session and then failed, and a fork copied the transcript first.
- Relative `additionalDirectories` entries are rejected with -32602.
**Session setup (v2)**
- Uses the SDK's `assertSupportedMcpServers`, which also rejects
duplicate MCP server names with -32602.
- Rejects an empty `prompt` with -32602.
- The v2 initialize response no longer advertises the AIR `asyncTasks`
capability or the `goal` entry, since the v2 app has no handlers for
`_session/async_task/stop` or `_session/goal`. v1 AIR is unchanged.
**Errors**
- `session/fork` of an unknown session returns -32002 with
`data.sessionId` instead of -32603. This matches the Claude SDK's
"Session … not found" and "Invalid sessionId" messages.
- `authenticate` with an unknown `methodId` returns -32602 instead of
-32603.
- `set_config_option` rejects a value whose type doesn't match the
advertised option type. A `type: "boolean"` option takes `true`/`false`,
and a select option takes a string. Model aliases such as `"opus"` are
still accepted.
- An AIR `session/delete` of a session that has no transcript now
succeeds instead of failing with a generic error.
**Prompts and commands**
- v1 `session/prompt` rejects `audio` and unknown content blocks with
-32602 before queueing them. Before, they were silently dropped.
- Advertised command names that contain whitespace are dropped, and of
duplicate names only the first is kept.
- The `/mcp:server:prompt` rewrite now accepts arguments that span
lines, and applies only to the first text block.
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>
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>
## 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.
## 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>
## 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.
## 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.
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.
## 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>
## 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>