Compare commits

..

1 Commits

Author SHA1 Message Date
Codex
98e7fe3231 chore: prototype opt-in RFD 2452 background shell bridge 2026-10-23 16:41:50 +02:00
27 changed files with 1001 additions and 1018 deletions

View File

@@ -1,3 +1,3 @@
{
".": "0.95.0"
".": "0.94.0"
}

View File

@@ -1,27 +1,5 @@
# Changelog
## [0.95.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.94.0...v0.95.0) (2026-10-23)
### Features
* end a turn at its result for AIR clients with backgroundSubagents ([#1262](https://github.com/agentclientprotocol/claude-agent-acp/issues/1262)) ([9b3a5c8](https://github.com/agentclientprotocol/claude-agent-acp/commit/9b3a5c891c75e78b985adb31f4e4b59a16ad2ebc))
* report read-only subagent configuration ([#1417](https://github.com/agentclientprotocol/claude-agent-acp/issues/1417)) ([6ab670b](https://github.com/agentclientprotocol/claude-agent-acp/commit/6ab670baaefb30aaac2606d8ee6524e08243e5b5))
* report RFD delegations as linked session messages ([#1405](https://github.com/agentclientprotocol/claude-agent-acp/issues/1405)) ([59e292e](https://github.com/agentclientprotocol/claude-agent-acp/commit/59e292e29f773cc6f03c0999993cbe8a2d97a7e3))
* serve experimental ACP v2 ([#1400](https://github.com/agentclientprotocol/claude-agent-acp/issues/1400)) ([4d378f9](https://github.com/agentclientprotocol/claude-agent-acp/commit/4d378f93f36d37ba5ef64af87b617706dd9c6e82))
* show messages typed in other Remote Control clients ([#1371](https://github.com/agentclientprotocol/claude-agent-acp/issues/1371)) ([5b11f47](https://github.com/agentclientprotocol/claude-agent-acp/commit/5b11f470b1c05b114c876b3b6e5b4df9441b1087))
* update to Claude Code 2.1.309 ([#1413](https://github.com/agentclientprotocol/claude-agent-acp/issues/1413)) ([a1521b6](https://github.com/agentclientprotocol/claude-agent-acp/commit/a1521b6196c58dbc5065f9c19fc9694273d3d435))
### Bug Fixes
* bump fast-uri from 3.1.8 to 3.1.9 ([#1410](https://github.com/agentclientprotocol/claude-agent-acp/issues/1410)) ([0fe22ae](https://github.com/agentclientprotocol/claude-agent-acp/commit/0fe22ae5a172b228a574665bb7136b900746041d))
* bump postcss from 8.5.30 to 8.5.31 in the minor group ([#1403](https://github.com/agentclientprotocol/claude-agent-acp/issues/1403)) ([ecaa2c9](https://github.com/agentclientprotocol/claude-agent-acp/commit/ecaa2c935b9f2b63c1846ca17c6c3398390fb0ef))
* bump the minor group across 1 directory with 2 updates ([#1416](https://github.com/agentclientprotocol/claude-agent-acp/issues/1416)) ([f7d6030](https://github.com/agentclientprotocol/claude-agent-acp/commit/f7d6030c04af23171058772480d55f6d2aee5298))
* IJAI-3744 keep a session running while its background tasks are open ([#1399](https://github.com/agentclientprotocol/claude-agent-acp/issues/1399)) ([e9f986b](https://github.com/agentclientprotocol/claude-agent-acp/commit/e9f986b8a941f5c1789078a39e474f247bd0db60))
* publish the session title as soon as the CLI has it ([#1406](https://github.com/agentclientprotocol/claude-agent-acp/issues/1406)) ([50cd76e](https://github.com/agentclientprotocol/claude-agent-acp/commit/50cd76e78dd9bb1ab0e4850a8d95431175ab5947))
* publish turns that Claude Code starts after end_turn as an async task in v1 ([#1390](https://github.com/agentclientprotocol/claude-agent-acp/issues/1390)) ([a1d0eee](https://github.com/agentclientprotocol/claude-agent-acp/commit/a1d0eeeaad3d67a0765f4b89e3d81c1af6ca0dbb))
## [0.94.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.93.0...v0.94.0) (2026-10-19)

View File

@@ -0,0 +1,47 @@
# Background work RFD experiment
This branch prototypes [RFD 2452](https://github.com/agentclientprotocol/agent-client-protocol/pull/2452)
at revision `33c473ba0723a0f6ffcfcbb54fd780c05df7b208`. It is an opt-in local
experiment, not a released protocol implementation.
Build with `npm ci --ignore-scripts` and `npm run build`. Launch with
`ACP_BACKGROUND_WORK_EXPERIMENT=1 node dist/index.js`. Omit the environment
variable to restore normal behavior. No package publication is needed.
The message-stream bridge works on ACP v1 and v2. It reuses the installed
native asynchronous-task adapter and its private negotiation internally. Known
background shells keep the existing tool call `in_progress`; state snapshots
reference that tool. `tool_call/cancel` is translated to the existing native
task stop request. A successful response is acceptance only. A native terminal
event finalizes the tool. ACP v1 represents cancellation as `failed` because its
tool status enum has no `cancelled`; ACP v2 uses `cancelled`.
Native replace-semantics task levels establish whether the list is complete.
Before a native level arrives, after a malformed level, or while any native task
cannot be mapped to an announced tool, `background` is `null`. This includes
unmapped monitors, scheduled tasks and subagents. The prototype does not claim
their full RFD support. Forks start with `background: []`. Reconnects remain
unreported until native current state and referenced entities are available.
This bridge does not upgrade the SDK schema. A client must accept the draft
fields and raw `tool_call/cancel`, and must treat repeated `idle` snapshots
without a stop reason as list changes rather than new completions. The private
negotiation can also select the adapter's existing client-specific tool rendering;
this is a bounded experiment, not general client capability negotiation.
Suggested manual evaluation: start a bounded background shell, finish the
foreground turn, cancel one shell, and separately allow a shell to finish while
idle. Verify that the tool remains in progress and listed until native evidence
ends it. Send foreground `session/cancel` while background work is listed, then
close the session. Reconnect and fork must not invent live tools from history.
Exercise pending permissions separately; deterministic protocol tests are not
evidence of live native permission or session-close behavior.
A tool already reported final before native background discovery is never revived.
That task remains unreported by this bridge. Native subagents also remain unreported
while associated, including an idle child whose own background state is unknown.
Codex list disappearance proves the process ended but supplies no exit outcome.
The existing native adapter reports this as stopped, represented here as cancelled
(v2) or failed (v1); only an explicit native command completion supplies a reliable
completed/failed outcome. This is a known limit of the experiment.

394
package-lock.json generated
View File

@@ -1,16 +1,16 @@
{
"name": "@agentclientprotocol/claude-agent-acp",
"version": "0.95.0",
"version": "0.94.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@agentclientprotocol/claude-agent-acp",
"version": "0.95.0",
"version": "0.94.0",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "2.0.1",
"@anthropic-ai/claude-agent-sdk": "0.3.309",
"@agentclientprotocol/sdk": "2.0.0",
"@anthropic-ai/claude-agent-sdk": "0.3.303",
"diff": "9.0.0",
"zod": "4.6.7"
},
@@ -18,18 +18,19 @@
"claude-agent-acp": "dist/index.js"
},
"devDependencies": {
"@anthropic-ai/sdk": "1.1.0",
"@anthropic-ai/sdk": "0.135.0",
"@eslint/js": "10.0.1",
"@tsconfig/node22": "22.0.6",
"@types/node": "26.6.5",
"@typescript-eslint/eslint-plugin": "8.72.0",
"@typescript-eslint/parser": "8.72.0",
"@typescript-eslint/eslint-plugin": "8.71.2",
"@typescript-eslint/parser": "8.71.2",
"ajv": "8.20.0",
"ajv-formats": "3.0.1",
"eslint": "10.13.0",
"eslint-config-prettier": "10.1.8",
"globals": "17.13.0",
"prettier": "3.9.10",
"ts-node": "10.9.2",
"typescript": "6.0.3",
"vitest": "5.0.5"
},
@@ -38,31 +39,31 @@
}
},
"node_modules/@agentclientprotocol/sdk": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-2.0.1.tgz",
"integrity": "sha512-v4E/NcBTGFEKmorGso8ysAjJTkNh5UMJPP4aDznyMNmnP9t10g/aCCo/Djy7RYaYZy5ZROFK3G5cEj9tDgGOTQ==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-2.0.0.tgz",
"integrity": "sha512-Za5gWOPQ44aBOdegDJZ+OjCAPn7POc0cw8BlFJQ1mmUY1OoUWxRi3+QZT4gdZnyEWgKXyfeGRb0Oo1ag9YkQ1w==",
"license": "Apache-2.0",
"peerDependencies": {
"zod": "^3.25.76 || ^4.0.0"
}
},
"node_modules/@anthropic-ai/claude-agent-sdk": {
"version": "0.3.309",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.309.tgz",
"integrity": "sha512-Tou0fXBkxKJDcgT09v3Iv/Jd7bNgapgfAiHxGPctAADPFQhidfhZbcv7at1cVL0Lon5sbs+OzP/pEOigw2TI0A==",
"version": "0.3.303",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.303.tgz",
"integrity": "sha512-XBql04V40xr+fmGJh8AmIpqRZKFNrs99BzXzzSVlx+uE5NjFCzKmkNKTdMD0DQtDvFJlijOLoh1siaH+au9Psw==",
"license": "SEE LICENSE IN README.md",
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.309",
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.309",
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.309",
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.309",
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.309",
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.309",
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.309",
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.309"
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.303",
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.303",
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.303",
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.303",
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.303",
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.303",
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.303",
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.303"
},
"peerDependencies": {
"@anthropic-ai/sdk": ">=0.93.0",
@@ -71,9 +72,9 @@
}
},
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": {
"version": "0.3.309",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.309.tgz",
"integrity": "sha512-qC93iDGEioaXqWsYazcZzNv3hd8d4VHkCFgb+AzvbkCWOzZHnRu1EqF4Iz5kCTLDs4VdMvCa1NkzlUwtTedBVw==",
"version": "0.3.303",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.303.tgz",
"integrity": "sha512-QAWV9Zd7i7FQDaUSwZNVXJirqCS9CSF4aPwbkGwLvLH83qcfFOmnyxWUg1H7dU6tJFLOCyDLQzc3xzUA4dakoA==",
"cpu": [
"arm64"
],
@@ -84,9 +85,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": {
"version": "0.3.309",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.309.tgz",
"integrity": "sha512-RvP0UOcYxn42c1lgByoMufHaMOH86U2ThGGFupOxBKATY4Q6WJd6M1CukQ/qygfgJ8lcxvK8GOOd3SIny4kQ8Q==",
"version": "0.3.303",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.303.tgz",
"integrity": "sha512-o931musaryuq04fMaEjceCEmgsBrLh39WvwsQWmv070h9jXS8EmWRFpdSILv1xrUFXiukRSq8yUDRVzg+FvTug==",
"cpu": [
"x64"
],
@@ -97,15 +98,12 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": {
"version": "0.3.309",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.309.tgz",
"integrity": "sha512-+8rQwzycuXTW2e/C+yotvP/SDiEDHxItdPvIriGAf2ixh7nPRjVuzHOj1tamgVO0J8nmpPvIAO4edzJ7VZCcAg==",
"version": "0.3.303",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.303.tgz",
"integrity": "sha512-c04i9i1BU3j9G3D7XdVzXTdM3/sDpQH+1o8jNcMPnztOTYbNFmH/cacgeX9x3N112r+5VhCEHXGILoe5GxVxeg==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
@@ -113,15 +111,12 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": {
"version": "0.3.309",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.309.tgz",
"integrity": "sha512-14ZD9hdUeobw+jr166YvCZyZwSYG3hDWi5S4yQxpSCrRuY/1z4Kl1AfymUlugiN3UT4Q1rif32K6GnjRJBvQdA==",
"version": "0.3.303",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.303.tgz",
"integrity": "sha512-a/OsLTEx9ly9hC26cXidg9mtTMA00SQnEiTN9P4IPNj64uSp/7uBnlWl0VeUoVFbgKYYq6XrGRcD6F1ilQsYxg==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
@@ -129,15 +124,12 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": {
"version": "0.3.309",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.309.tgz",
"integrity": "sha512-96G8/uTArlvTTg0qGtiP36W76Ug/YT0gMBh8KX7J2RigdOTrk6QkHtUeCzrw6yff5hEIzecN7EWNw6uh9USo4A==",
"version": "0.3.303",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.303.tgz",
"integrity": "sha512-lsm1sX2+9Wos8glBbn86M8leFuK60LKB1cJdS0b2HiTSwiDCqpUKYRGJNUlW1JdUTwJB5nDv6S8/FOJExm1/tw==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
@@ -145,15 +137,12 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": {
"version": "0.3.309",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.309.tgz",
"integrity": "sha512-9ViOt4MGMiE2h8V+JUPR5l9F0ZTFuhKC3jawIL/MrMfYY1m2Gt3gcMuP7h7YK914a4tmpXrmITWrpTOu1WOZQQ==",
"version": "0.3.303",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.303.tgz",
"integrity": "sha512-yixAa3ZOZd1hiI4mpoukzT9TnuDCzSEHyh7pHcbYTRmdRE2y8bAOmI0eiMFJPivnG4iyGhS2C4XNAe6hAjl30w==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
@@ -161,9 +150,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": {
"version": "0.3.309",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.309.tgz",
"integrity": "sha512-lDsmw5hguDF7KNMG2OvvKlG0Zgs9bKlrX9Gs2Rv/NjLVl0PIy1TUsh7byOsUR4pXwoMU1toyU1CKqBc/Au8hHw==",
"version": "0.3.303",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.303.tgz",
"integrity": "sha512-AnEseiw5E1JF3XRaGIZN880V/i2OxCaTOQJF9F1+oELleSdvLnCUxCE6/baGvaZjrgvffO9YbYChBpgUKj9j7A==",
"cpu": [
"arm64"
],
@@ -174,9 +163,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": {
"version": "0.3.309",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.309.tgz",
"integrity": "sha512-otGHQimz6F++6SajGqSAKBclRPGTn60cx+8u3dnhMa5yovKZ5SgChFgstFsiDIWr/ZBqpJysvFemSgHX61mlmQ==",
"version": "0.3.303",
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.303.tgz",
"integrity": "sha512-j5kABFEQ6x6T5AB/glmn0r9VwZRbzcH+nSEOWglB69ODNfkZA2btjcz9r794+2BLqJ9QAyswiYWcOX4sDz/RmQ==",
"cpu": [
"x64"
],
@@ -187,10 +176,10 @@
]
},
"node_modules/@anthropic-ai/sdk": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-1.1.0.tgz",
"integrity": "sha512-Op/fKMsfVwFCfObc87leLOFlORE0RjOvLzRzPCWWLI7Ww6bobpKR6YmYC8ZYgulRh+m2a/3/61VUXoWHFtCXIw==",
"license": "Apache-2.0",
"version": "0.135.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.135.0.tgz",
"integrity": "sha512-WWrdL57JhU5KoynpTjt9GXK6k7ud9nPw2uw/Trn+oBXh6s0KpjhAccWZm2Qxw08ARYG1UDQj5iC+JO42j1rt/g==",
"license": "MIT",
"dependencies": {
"@opentelemetry/api": "^1.4.1",
"json-schema-to-ts": "^3.1.1",
@@ -241,6 +230,19 @@
"keyv": "^5.6.0"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "0.3.9"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
@@ -452,6 +454,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@keyv/bigmap": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz",
@@ -518,9 +531,9 @@
}
},
"node_modules/@opentelemetry/api": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.10.0.tgz",
"integrity": "sha512-lMkyes8iHrG+mk6WH7X70MdCLI2RS0FwNBY1PyQ8UemaijE2m5mv5A0Niy3HtbYTFrgkUEQjFMXmSAk2hQDuPQ==",
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz",
"integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==",
"license": "Apache-2.0",
"engines": {
"node": ">=8.0.0"
@@ -821,6 +834,34 @@
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
"license": "MIT"
},
"node_modules/@tsconfig/node10": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.13.tgz",
"integrity": "sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node12": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node14": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node16": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node22": {
"version": "22.0.6",
"resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.6.tgz",
@@ -878,17 +919,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.0.tgz",
"integrity": "sha512-aysKgysPX9XOv/dk95UkvUDe++Q3n3dJPG9CEWT3RUccxfkPJ+Qn6nhKPDhZXU6Ofo9kgwGWwKyQCnZIXTgngg==",
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.2.tgz",
"integrity": "sha512-K72/pvT7qpm8q3k8G5u93NCSN1RcLxX5h7Gw0bQpbMXviBHeY6Nx1WEwmk9wP+eKTSQ8ecxvcgt/ZX/rB938xQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.72.0",
"@typescript-eslint/type-utils": "8.72.0",
"@typescript-eslint/utils": "8.72.0",
"@typescript-eslint/visitor-keys": "8.72.0",
"@typescript-eslint/scope-manager": "8.71.2",
"@typescript-eslint/type-utils": "8.71.2",
"@typescript-eslint/utils": "8.71.2",
"@typescript-eslint/visitor-keys": "8.71.2",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -901,22 +942,22 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.72.0",
"@typescript-eslint/parser": "^8.71.2",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.0.tgz",
"integrity": "sha512-0O+x3ynciD6zuXb1CKSu165haQV6ey53DKhHHUnujP7U+XlltJG+/keT0Xhd1HXCze+IHf7Tvt39ovyepX/LNw==",
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.2.tgz",
"integrity": "sha512-aTtmZjp/x3gsGHkDs04/1I8kNogBJkpQaeGaN0PfiG43NzDEmW2n+OP5im9wfsvyX0yP1UZWBrAGtDBfH9jEVw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.72.0",
"@typescript-eslint/types": "8.72.0",
"@typescript-eslint/typescript-estree": "8.72.0",
"@typescript-eslint/visitor-keys": "8.72.0",
"@typescript-eslint/scope-manager": "8.71.2",
"@typescript-eslint/types": "8.71.2",
"@typescript-eslint/typescript-estree": "8.71.2",
"@typescript-eslint/visitor-keys": "8.71.2",
"debug": "^4.4.3"
},
"engines": {
@@ -932,14 +973,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.0.tgz",
"integrity": "sha512-bzHngBbmIgzj6TsTjVUCntGx6FFLgE7mPjpO0PzHycOaZHXqS43XnrkFIHRS16LLr/xO6FHSXG3GJFo/F4bSBQ==",
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.2.tgz",
"integrity": "sha512-Pd68GsLVh5ZXlcEccFPdhSOvtHZ/2WU0XsA8zmXOS3F158lVlcTLhfQ6qvtL1RzbMie0Kp8xtbVwAg4nejKPHw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.72.0",
"@typescript-eslint/types": "^8.72.0",
"@typescript-eslint/tsconfig-utils": "^8.71.2",
"@typescript-eslint/types": "^8.71.2",
"debug": "^4.4.3"
},
"engines": {
@@ -954,14 +995,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.0.tgz",
"integrity": "sha512-JewPyJOYcXBSAS7PIgufscHOwZqhTGyBRiQJhhuDln66tMchxvvt7nJ90JKKIoOelmdHA3kAQbm9cEUHc4JSEA==",
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.2.tgz",
"integrity": "sha512-YF4wgg5iUX5Obywoac27B56FhozMNFhdJSbh11H5GBAwmBtfyRhmgTQqW9tRNNmHDvsZ2CX6UGzoz+YfYn2Leg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.72.0",
"@typescript-eslint/visitor-keys": "8.72.0"
"@typescript-eslint/types": "8.71.2",
"@typescript-eslint/visitor-keys": "8.71.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -972,9 +1013,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.0.tgz",
"integrity": "sha512-0vbxCf7xLVPSjcOdm6eQbgViH8EDJ8xnl1laLT9yoqSz2YmcSfSg5Vdl0JUvZf6rcuJ0ZivJIuMD4edTg8obaA==",
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.2.tgz",
"integrity": "sha512-jz49TAWokm/7yWD9/o837nYax9DyALxIPOImdImvVzo6canBCZYzin8zJyqNR5k1j9K1DE0bVvr0XYB+99uAdg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -989,15 +1030,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.0.tgz",
"integrity": "sha512-gEXxmmbSTSOVTC37IYYvremOM31v+UN2W772bJxmVYPhAOPSUxtacyb7wkm1EhkYKU6KzFJsdU44DCV4to0LWw==",
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.2.tgz",
"integrity": "sha512-cC8UaalCViJkb3rH/xLIvidEwoFRtMZMUqDjjJMGEKaTFwYRHYBcoczlEGHuvkHEO8svRDrgZ0yQgOArFmwang==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.72.0",
"@typescript-eslint/typescript-estree": "8.72.0",
"@typescript-eslint/utils": "8.72.0",
"@typescript-eslint/types": "8.71.2",
"@typescript-eslint/typescript-estree": "8.71.2",
"@typescript-eslint/utils": "8.71.2",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -1014,9 +1055,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.0.tgz",
"integrity": "sha512-Adfoy59kxNEjT5E5fnyrp7GpHYuWs8ONbdohu4ez6IU46qDq8LltG90tLmkucZB2uhYL8mqBjtXqGd8yhfjXqQ==",
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.2.tgz",
"integrity": "sha512-y5Ukl3oVp7e/VGxqIoEXoeb/Roj71PAJ4WG/N/RGj+eeDvKRLM15kGr9MijLbN0i03S52zxu31Z98E5gWChJKw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1028,16 +1069,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.0.tgz",
"integrity": "sha512-gwLfFgWcw15tN2KXDCJvxEOQjPn+P3j/n3eSqkZ5sX4VpW9Cosodr+tWwBZRvJT3TcKx6HnPRGZMCNAiP1Xhug==",
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.2.tgz",
"integrity": "sha512-DgNOEsl4pdYzwSL+N/Cl33tX1Hlw3FvId73LnKXfq7jnrF2viOEn7X+P7RBkkSWCVm92gnWd28GIC5vJuLREeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.72.0",
"@typescript-eslint/tsconfig-utils": "8.72.0",
"@typescript-eslint/types": "8.72.0",
"@typescript-eslint/visitor-keys": "8.72.0",
"@typescript-eslint/project-service": "8.71.2",
"@typescript-eslint/tsconfig-utils": "8.71.2",
"@typescript-eslint/types": "8.71.2",
"@typescript-eslint/visitor-keys": "8.71.2",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -1056,16 +1097,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.0.tgz",
"integrity": "sha512-0up3oArJZBYnzOaEXKo8DmI43QvMZyPoFEt9LWzRJcyl/RcJjfKKPebknN1VbTgXBf3M/KggUh5uUMdURLnseg==",
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.2.tgz",
"integrity": "sha512-RyKa1G1M9rKHDhrk+HJtiF1MvLD5Ux2JW3TSkzw+02pRALdpJb+QzG1Y5em+LvCgzGwgg8H7mBRFKljNz7ImPw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.72.0",
"@typescript-eslint/types": "8.72.0",
"@typescript-eslint/typescript-estree": "8.72.0"
"@typescript-eslint/scope-manager": "8.71.2",
"@typescript-eslint/types": "8.71.2",
"@typescript-eslint/typescript-estree": "8.71.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1080,13 +1121,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.0.tgz",
"integrity": "sha512-E2mroviasaWecDzTGiGMtuEguuLVOeRggnrIn93CJuxYJzQBHbyymlT08aVQydbBal5538o+JkBKXh2sRuCyIA==",
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.2.tgz",
"integrity": "sha512-zozlN0UA32bSG2zkKAgQyRYSbEISVoyU6vvkKYo2aSny74D8Jh4e+POkJ6Lk0dlezZi5x7TtD+88Xb06IB8nhg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.72.0",
"@typescript-eslint/types": "8.71.2",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -1196,6 +1237,19 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/acorn-walk": {
"version": "8.3.5",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
"integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.11.0"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/ajv": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
@@ -1229,6 +1283,13 @@
}
}
},
"node_modules/arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true,
"license": "MIT"
},
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
@@ -1428,6 +1489,13 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -1977,9 +2045,9 @@
"license": "Unlicense"
},
"node_modules/fast-uri": {
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz",
"integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==",
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
"integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
"funding": [
{
"type": "github",
@@ -2380,9 +2448,9 @@
"license": "ISC"
},
"node_modules/jose": {
"version": "6.2.13",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.13.tgz",
"integrity": "sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ==",
"version": "6.2.12",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz",
"integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==",
"license": "MIT",
"peer": true,
"funding": {
@@ -2745,6 +2813,13 @@
"@jridgewell/sourcemap-codec": "^1.6.0"
}
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true,
"license": "ISC"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -3557,6 +3632,60 @@
"typescript": ">=4.8.4"
}
},
"node_modules/ts-node": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
"@tsconfig/node16": "^1.0.2",
"acorn": "^8.4.1",
"acorn-walk": "^8.1.1",
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"v8-compile-cache-lib": "^3.0.1",
"yn": "3.1.1"
},
"bin": {
"ts-node": "dist/bin.js",
"ts-node-cwd": "dist/bin-cwd.js",
"ts-node-esm": "dist/bin-esm.js",
"ts-node-script": "dist/bin-script.js",
"ts-node-transpile-only": "dist/bin-transpile.js",
"ts-script": "dist/bin-script-deprecated.js"
},
"peerDependencies": {
"@swc/core": ">=1.2.50",
"@swc/wasm": ">=1.2.50",
"@types/node": "*",
"typescript": ">=2.7"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
},
"@swc/wasm": {
"optional": true
}
}
},
"node_modules/ts-node/node_modules/diff": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
"integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -3644,6 +3773,13 @@
"punycode": "^2.1.0"
}
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true,
"license": "MIT"
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -3861,6 +3997,16 @@
"license": "ISC",
"peer": true
},
"node_modules/yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@@ -3,7 +3,7 @@
"publishConfig": {
"access": "public"
},
"version": "0.95.0",
"version": "0.94.0",
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
"main": "dist/lib.js",
"types": "dist/lib.d.ts",
@@ -65,24 +65,25 @@
"node": ">=22"
},
"dependencies": {
"@agentclientprotocol/sdk": "2.0.1",
"@anthropic-ai/claude-agent-sdk": "0.3.309",
"@agentclientprotocol/sdk": "2.0.0",
"@anthropic-ai/claude-agent-sdk": "0.3.303",
"diff": "9.0.0",
"zod": "4.6.7"
},
"devDependencies": {
"@anthropic-ai/sdk": "1.1.0",
"@anthropic-ai/sdk": "0.135.0",
"@eslint/js": "10.0.1",
"@tsconfig/node22": "22.0.6",
"@types/node": "26.6.5",
"@typescript-eslint/eslint-plugin": "8.72.0",
"@typescript-eslint/parser": "8.72.0",
"@typescript-eslint/eslint-plugin": "8.71.2",
"@typescript-eslint/parser": "8.71.2",
"ajv": "8.20.0",
"ajv-formats": "3.0.1",
"eslint": "10.13.0",
"eslint-config-prettier": "10.1.8",
"globals": "17.13.0",
"prettier": "3.9.10",
"ts-node": "10.9.2",
"typescript": "6.0.3",
"vitest": "5.0.5"
}

View File

@@ -353,7 +353,6 @@ import {
buildEffortConfigOption,
EFFORT_CONFIG_ID,
effortFlagSettings,
effortLevelName,
mergeEffortSettings,
settingsEffortForModel,
} from "./session-effort.js";
@@ -4411,7 +4410,10 @@ export class ClaudeAcpAgent {
} else {
await session.query.stopTask(params.asyncTaskId);
}
await asyncTasks.taskStopped(params.asyncTaskId);
// Acceptance is separate from the native terminal event in the RFD experiment.
if (process.env.ACP_BACKGROUND_WORK_EXPERIMENT !== "1") {
await asyncTasks.taskStopped(params.asyncTaskId);
}
return { stopped: true };
} catch (error) {
asyncTasks.releaseStop(params.asyncTaskId);
@@ -5877,28 +5879,9 @@ export class ClaudeAcpAgent {
// turn activation and settlement stay driven by echoes/results/idle.
// (Raw-mode emission above still forwards these frames.)
if ((message as { type: string }).type === "command_lifecycle") {
const frame = message as unknown as {
command_uuid: string;
state: string;
wakeup_source?: string;
scheduled_for?: number;
};
const frame = message as unknown as { command_uuid: string; state: string };
switch (frame.state) {
case "started": {
// CLIs 2.1.307+ (capability lifecycle_wakeup_source_v1) mark a
// command that a scheduler fire started. With no prompt of ours
// open, it starts an autonomous turn: name the fire as its cause,
// whether `session_state_changed` already opened the task or not.
const cause = scheduledFireCause(frame);
if (
cause &&
asyncTasks.enabled &&
!session.cancelled &&
!session.activeTurn &&
!firstUnsettledQueuedTurn()
) {
await asyncTasks.autonomousTurnStarted(cause);
}
// Remember dispatch on the live turn so a cancel() that orphans
// it seeds the right state (see Turn.commandStarted)...
const queued = findUnsettledTurn(frame.command_uuid);
@@ -6819,6 +6802,19 @@ export class ClaudeAcpAgent {
case "control_request_progress":
break;
case "background_tasks_changed":
if (process.env.ACP_BACKGROUND_WORK_EXPERIMENT === "1") {
await this.client.sessionUpdate({
sessionId: params.sessionId,
update: {
sessionUpdate: "session_info_update",
_meta: {
backgroundWorkExperiment: {
taskIds: message.tasks.map((task) => task.task_id),
},
},
},
});
}
await asyncTasks.backgroundTasksChanged(message.tasks);
// A level signal: the full live background-task set on every
// membership change, with REPLACE semantics. Used only to
@@ -7649,11 +7645,6 @@ export class ClaudeAcpAgent {
// it) can all be tagged with the same, replay-stable id.
if (message.event.type === "message_start") {
currentStreamMessageId = message.event.message.id || undefined;
await subagents.reportModel(
message.parent_tool_use_id,
message.event.message.model,
sendUpdate,
);
// A new top-level message starts: clear any streamed-content
// residue from a prior message that never reached its
// consolidated reset — a cancelled turn breaks out before the
@@ -8020,14 +8011,6 @@ export class ClaudeAcpAgent {
break;
}
if (message.type === "assistant") {
await subagents.reportModel(
message.parent_tool_use_id,
message.message.model,
sendUpdate,
);
}
// Snapshot the latest top-level assistant usage and model so the
// next `result` can emit a usage_update tied to the right context
// window. Subagent messages are excluded to keep the snapshot
@@ -8629,7 +8612,13 @@ export class ClaudeAcpAgent {
session.cancelled = true;
// The interrupt below makes Claude Code withdraw the requests that are open
// now (see `withPendingUserInput`).
for (const input of session.openUserInputs ?? []) input.cancelled = true;
for (const input of session.openUserInputs ?? []) {
if (
process.env.ACP_BACKGROUND_WORK_EXPERIMENT !== "1" ||
blocksForeground(session, input.agentID)
)
input.cancelled = true;
}
// Before anything awaits, so a model request that the consumer reads in
// the meantime is already judged against these prompts.
session.cancelledPrompts ??= new Set();
@@ -10153,69 +10142,6 @@ export class ClaudeAcpAgent {
};
}
private subagentConfigurationHook(sessionId: string): HookCallback {
return async (input) => {
const session = this.sessions[sessionId];
if (!session || !input.agent_id) return { continue: true };
const options: SessionConfigOption[] = [];
if (input.permission_mode) {
const mode = session.modes.availableModes.find((mode) => mode.id === input.permission_mode);
options.push(
SessionModeManager.configOption({
currentModeId: input.permission_mode,
availableModes: [mode ?? { id: input.permission_mode, name: input.permission_mode }],
}),
);
}
if (input.hook_event_name === "PostModelSwitch" && input.to_model) {
options.push({
id: MODEL_CONFIG_ID,
name: "Model",
category: "model",
type: "select",
currentValue: input.to_model,
options: [{ value: input.to_model, name: input.to_model }],
});
}
if (input.effort?.level) {
options.push({
id: EFFORT_CONFIG_ID,
name: "Effort",
description: "Current reasoning effort",
category: "thought_level",
type: "select",
currentValue: input.effort.level,
options: [{ value: input.effort.level, name: effortLevelName(input.effort.level) }],
});
}
if (input.agent_type) {
const agent = session.agents.find((agent) => agent.name === input.agent_type);
options.push({
id: AGENT_CONFIG_ID,
name: "Agent",
description: "Subagent type",
type: "select",
currentValue: input.agent_type,
options: [
{
value: input.agent_type,
name: input.agent_type,
...(agent?.description ? { description: agent.description } : {}),
},
],
});
}
await session.nativeSubagentRuntime
?.reportConfiguration(input.agent_id, options)
.catch((error) =>
this.logger.error(
`Session ${sessionId}: failed to report subagent configuration: ${error}`,
),
);
return { continue: true };
};
}
/**
* The PreToolUse hook of a v2 connection, for Edit and Write: the tool call
* shows the exact patch of the change before the tool runs.
@@ -11478,10 +11404,6 @@ export class ClaudeAcpAgent {
`[session/query] sessionId=${sessionId} resume=${creationOpts?.resume ?? "none"} apiType=${resolvedProvider?.apiType ?? "native"} baseUrl=${resolvedProvider?.baseUrl ?? "native"}`,
);
const subagentConfigHook =
subagentForm(this.clientCapabilities) === "rfd"
? this.subagentConfigurationHook(sessionId)
: undefined;
const options: Options = {
systemPrompt,
settingSources: ["user", "project", "local"],
@@ -11496,7 +11418,10 @@ export class ClaudeAcpAgent {
// `stopBackgroundWorkflows` stops instead. Background commands survive an
// interrupt either way. Before `userProvidedOptions`, so a client's own
// `perTaskStopAffordance` wins.
...(subagentForm(this.clientCapabilities) === "rfd" ? { perTaskStopAffordance: true } : {}),
...(subagentForm(this.clientCapabilities) === "rfd" ||
process.env.ACP_BACKGROUND_WORK_EXPERIMENT === "1"
? { perTaskStopAffordance: true }
: {}),
// One predicted follow-up prompt per turn, published as
// `session_info_update._meta.promptSuggestions`. It rides the turn's own
// prompt cache, and the user can turn it off in settings.json
@@ -11550,33 +11475,14 @@ export class ClaudeAcpAgent {
tools,
hooks: {
...userProvidedOptions?.hooks,
...(subagentConfigHook
? {
SubagentStart: [
...(userProvidedOptions?.hooks?.SubagentStart ?? []),
{ hooks: [subagentConfigHook] },
],
SubagentStop: [
...(userProvidedOptions?.hooks?.SubagentStop ?? []),
{ hooks: [subagentConfigHook] },
],
PostToolUseFailure: [
...(userProvidedOptions?.hooks?.PostToolUseFailure ?? []),
{ hooks: [subagentConfigHook] },
],
}
: {}),
// On v2, an Edit or Write shows its exact patch before it runs, also
// when no permission request shows it (`fileChangePreviewHook`): a v2
// diff cannot hold the input's snippet.
...(this.v2 || subagentConfigHook
...(this.v2
? {
PreToolUse: [
...(userProvidedOptions?.hooks?.PreToolUse || []),
...(subagentConfigHook ? [{ hooks: [subagentConfigHook] }] : []),
...(this.v2
? [{ matcher: "Edit|Write", hooks: [this.fileChangePreviewHook(sessionId)] }]
: []),
{ matcher: "Edit|Write", hooks: [this.fileChangePreviewHook(sessionId)] },
],
}
: {}),
@@ -11584,7 +11490,6 @@ export class ClaudeAcpAgent {
...(userProvidedOptions?.hooks?.PostToolUse || []),
{
hooks: [
...(subagentConfigHook ? [subagentConfigHook] : []),
createPostToolUseHook({
onEnterPlanMode: async () => {
await this.sessionModes.publishCurrent(sessionId, "plan");
@@ -11608,11 +11513,9 @@ export class ClaudeAcpAgent {
...(userProvidedOptions?.hooks?.PostModelSwitch || []),
{
hooks: [
...(subagentConfigHook ? [subagentConfigHook] : []),
async (input) => {
if (
input.hook_event_name === "PostModelSwitch" &&
!input.agent_id &&
input.source !== "sdk" &&
input.source !== "resume"
) {
@@ -14095,29 +13998,3 @@ function getMatchingModelUsage(modelUsage: Record<string, ModelUsage>, currentMo
return { key: bestKey, usage: modelUsage[bestKey] };
}
}
/**
* What a `command_lifecycle` "started" frame says started the command: a
* ScheduleWakeup (dynamic `/loop`) wakeup, with the time it was set for in
* epoch milliseconds, or another scheduled task such as a cron job. Undefined
* for any other command, and for a value this adapter does not know. The time
* is given in UTC because the agent does not know the client's time zone.
*/
function scheduledFireCause(frame: {
wakeup_source?: string;
scheduled_for?: number;
}): string | undefined {
switch (frame.wakeup_source) {
case "loop_wakeup": {
const scheduledFor =
typeof frame.scheduled_for === "number" ? new Date(frame.scheduled_for) : undefined;
return scheduledFor && !Number.isNaN(scheduledFor.getTime())
? `A scheduled wakeup fired (set for ${scheduledFor.toISOString()}).`
: "A scheduled wakeup fired.";
}
case "schedule_wakeup":
return "A scheduled task fired.";
default:
return undefined;
}
}

View File

@@ -458,25 +458,18 @@ export class AsyncTaskRuntime {
* example to answer a task notification. ACP v1 has no session state, and
* the client already got the stop reason of its last prompt, so this task is
* the only sign that the agent works. The tool calls of the turn are in the
* transcript already, so the task is not shown there.
*
* [cause] names what started the turn when Claude Code says so, for example
* "A scheduled wakeup fired." It replaces the description of a task that is
* already open; without it, an open task is left as it is.
* transcript already, so the task is not shown there. Does nothing while
* such a task is open.
*/
async autonomousTurnStarted(cause?: string): Promise<void> {
if (!this.enabled) return;
if (this.autonomousTurn) {
if (cause) await this.publishProgress(this.autonomousTurn, { description: cause });
return;
}
async autonomousTurnStarted(): Promise<void> {
if (!this.enabled || this.autonomousTurn) return;
const task = this.task(`autonomous-turn-${randomUUID()}`);
task.autonomousTurn = true;
task.startedObserved = true;
task.name = "Claude is working";
task.taskType = "turn";
task.description =
cause ?? this.lastNotificationSummary ?? "Claude Code started a turn without a prompt.";
this.lastNotificationSummary ?? "Claude Code started a turn without a prompt.";
task.showInTranscript = false;
this.lastNotificationSummary = undefined;
this.autonomousTurn = task;

View File

@@ -0,0 +1,362 @@
/** Opt-in wire prototype for RFD 2452 over the installed native task adapters. */
type Json = Record<string, unknown>;
type Task = {
id: string;
toolCallId?: string;
canStop: boolean;
final: boolean;
supported: boolean;
};
type Session = {
state: string;
known: boolean;
level?: Set<string>;
tasks: Map<string, Task>;
tools: Set<string>;
finalTools: Set<string>;
children: Set<string>;
};
function object(value: unknown): Json {
return value !== null && typeof value === "object" && !Array.isArray(value)
? (value as Json)
: {};
}
export class BackgroundWorkExperiment {
private sessions = new Map<string, Session>();
private requests = new Map<unknown, { method: string; sessionId?: string }>();
private cancellations = new Set<unknown>();
private protocol = 1;
private session(id: string): Session {
let session = this.sessions.get(id);
if (!session) {
session = {
state: "idle",
known: false,
tasks: new Map(),
tools: new Set(),
finalTools: new Set(),
children: new Set(),
};
this.sessions.set(id, session);
}
return session;
}
private notification(sessionId: string, update: Json): Json {
return {
jsonrpc: "2.0",
method: "session/update",
params: { sessionId, update },
};
}
snapshot(sessionId: string): Json {
const session = this.session(sessionId);
const live = [...session.tasks.values()].filter(
(task) => !task.final && session.level?.has(task.id),
);
const complete =
session.known &&
session.children.size === 0 &&
[...(session.level ?? [])].every((id) => {
const task = session.tasks.get(id);
return (
task &&
task.supported &&
!task.final &&
task.toolCallId !== undefined &&
session.tools.has(task.toolCallId) &&
!session.finalTools.has(task.toolCallId)
);
});
return this.notification(sessionId, {
sessionUpdate: "state_update",
state: session.state,
background: complete
? live.map((task) => ({
type: "tool_call",
toolCallId: task.toolCallId,
}))
: null,
});
}
inbound(message: Json): { forward?: Json; send: Json[] } {
const params = object(message["params"]);
const sessionId = typeof params["sessionId"] === "string" ? params["sessionId"] : undefined;
const send: Json[] = [];
if (message["method"] === "initialize") {
this.protocol = typeof params["protocolVersion"] === "number" ? params["protocolVersion"] : 1;
const capabilities = object(params["clientCapabilities"]),
meta = object(capabilities["_meta"]);
const jetbrains = object(meta["jetbrains"]),
air = object(jetbrains["air"]);
const old = Array.isArray(air["capabilities"]) ? air["capabilities"] : [];
// The native task adapters already implement these private capabilities.
message = {
...message,
params: {
...params,
clientCapabilities: {
...capabilities,
_meta: {
...meta,
jetbrains: {
...jetbrains,
air: {
...air,
version: 1,
capabilities: [...new Set([...old, "asyncTasks", "backgroundSubagents"])],
},
},
},
},
},
};
}
if (["session/resume", "session/load"].includes(String(message["method"])) && sessionId) {
const session = this.session(sessionId);
session.known = false;
delete session.level;
session.tools.clear();
}
if (message["method"] === "tool_call/cancel") {
const session = sessionId ? this.session(sessionId) : undefined;
const task =
session &&
[...session.tasks.values()].find(
(task) =>
!task.final &&
task.supported &&
task.canStop &&
task.toolCallId === params["toolCallId"] &&
task.toolCallId !== undefined &&
session.tools.has(task.toolCallId) &&
!session.finalTools.has(task.toolCallId),
);
if (!task)
return {
send: [
{
jsonrpc: "2.0",
id: message["id"],
error: {
code: -32602,
message: "Tool call is not cancellable background work",
},
},
],
};
this.cancellations.add(message["id"]);
return {
forward: {
...message,
method: "_session/async_task/stop",
params: { sessionId, asyncTaskId: task.id },
},
send,
};
}
if (message["id"] !== undefined && typeof message["method"] === "string")
this.requests.set(message["id"], {
method: message["method"],
sessionId,
});
if (message["method"] === "session/prompt" && sessionId && this.protocol < 2) {
this.session(sessionId).state = "running";
send.push(this.snapshot(sessionId));
}
return { forward: message, send };
}
outbound(message: Json): Json[] {
if (!message["method"] && this.cancellations.delete(message["id"])) {
if (message["error"]) return [message];
return object(message["result"])["stopped"] === true
? [{ ...message, result: {} }]
: [
{
jsonrpc: "2.0",
id: message["id"],
error: {
code: -32602,
message: "Native cancellation was not accepted",
},
},
];
}
const request = !message["method"] ? this.requests.get(message["id"]) : undefined;
if (request) {
this.requests.delete(message["id"]);
const result = object(message["result"]);
const id = typeof result["sessionId"] === "string" ? result["sessionId"] : request.sessionId;
if (id && !message["error"]) {
if (request.method === "session/new" || request.method === "session/fork") {
if (request.method === "session/fork") {
this.session(id).known = true;
this.session(id).level = new Set();
}
return [message, this.snapshot(id)];
}
if (request.method === "session/resume" || request.method === "session/load")
return [this.snapshot(id), message];
if (request.method === "session/close") this.sessions.delete(id);
}
if (id && request.method === "session/prompt" && this.protocol < 2) {
this.session(id).state = "idle";
return [this.snapshot(id), message];
}
}
const params = object(message["params"]),
update = object(params["update"]);
const sessionId = typeof params["sessionId"] === "string" ? params["sessionId"] : undefined;
if (message["method"] !== "session/update" || !sessionId) return [message];
const session = this.session(sessionId);
const marker = object(object(update["_meta"])["backgroundWorkExperiment"]);
if (update["sessionUpdate"] === "session_info_update" && "taskIds" in marker) {
if (
!Array.isArray(marker["taskIds"]) ||
!marker["taskIds"].every((id) => typeof id === "string" && id.length > 0)
) {
session.known = false;
} else {
session.level = new Set(marker["taskIds"] as string[]);
session.known = true;
}
return [this.snapshot(sessionId)];
}
if (update["sessionUpdate"] === "state_update") {
if (typeof update["state"] === "string") session.state = update["state"];
return [
{
...message,
params: {
...params,
update: {
...update,
background: object(object(this.snapshot(sessionId)["params"])["update"])[
"background"
],
},
},
},
];
}
if (
["subagent_update", "_subagent_update", "subagent_spawned", "subagent_state_update"].includes(
String(update["sessionUpdate"]),
)
) {
const id = String(update["sessionId"] ?? update["subagentSessionId"] ?? "unknown");
const state =
typeof update["state"] === "string" ? update["state"] : object(update["state"])["state"];
if (["completed", "failed", "cancelled"].includes(String(state))) session.children.delete(id);
else session.children.add(id);
return [message, this.snapshot(sessionId)];
}
if (update["sessionUpdate"] === "tool_call" || update["sessionUpdate"] === "tool_call_update") {
const id = typeof update["toolCallId"] === "string" ? update["toolCallId"] : "";
if (id && (this.protocol >= 2 || update["sessionUpdate"] === "tool_call"))
session.tools.add(id);
const task = [...session.tasks.values()].find(
(task) =>
!task.final && task.supported && task.toolCallId === id && !session.finalTools.has(id),
);
if (!task && ["completed", "failed", "cancelled"].includes(String(update["status"])))
session.finalTools.add(id);
return task
? [
{
...message,
params: {
...params,
update: {
...update,
status: "in_progress",
capabilities: task.canStop ? { cancel: {} } : {},
},
},
},
]
: [message];
}
if (
typeof update["sessionUpdate"] !== "string" ||
!update["sessionUpdate"].replace(/^_/, "").startsWith("async_task_")
)
return [message];
const id = typeof update["asyncTaskId"] === "string" ? update["asyncTaskId"] : undefined;
if (!id) {
session.known = false;
return [message, this.snapshot(sessionId)];
}
let task = session.tasks.get(id);
if (!task) {
task = { id, canStop: false, final: false, supported: false };
session.tasks.set(id, task);
}
if (String(update["sessionUpdate"]).replace(/^_/, "") === "async_task_spawned")
task.supported = update["taskType"] === "shell";
if (typeof update["toolCallId"] === "string") task.toolCallId = update["toolCallId"];
if (update["canStop"] === true) task.canStop = true;
const terminal = ["completed", "failed", "stopped"].includes(String(update["state"]));
if (!terminal && session.level && !session.level.has(id)) session.known = false;
const wasFinal = task.final;
if (terminal) {
task.final = true;
session.level?.delete(id);
}
if (wasFinal) return [this.snapshot(sessionId)];
if (
!task.supported ||
!task.toolCallId ||
!session.tools.has(task.toolCallId) ||
session.finalTools.has(task.toolCallId)
)
return [message, this.snapshot(sessionId)];
const updates: Json[] = [];
if (!wasFinal)
updates.push(
this.notification(sessionId, {
sessionUpdate: "tool_call_update",
toolCallId: task.toolCallId,
status: terminal
? update["state"] === "stopped"
? this.protocol < 2
? "failed"
: "cancelled"
: update["state"]
: "in_progress",
capabilities: terminal ? null : task.canStop ? { cancel: {} } : {},
}),
);
if (terminal) session.finalTools.add(task.toolCallId);
return [...updates, this.snapshot(sessionId)];
}
}
/** Draft messages are added at the stream boundary, outside the installed SDK schema. */
export function backgroundWorkStream<T>(stream: {
readable: ReadableStream<T>;
writable: WritableStream<T>;
}): typeof stream {
if (process.env["ACP_BACKGROUND_WORK_EXPERIMENT"] !== "1") return stream;
const experiment = new BackgroundWorkExperiment(),
writer = stream.writable.getWriter();
const send = async (messages: Json[]) => {
for (const message of messages) await writer.write(message as T);
};
return {
readable: stream.readable.pipeThrough(
new TransformStream<T, T>({
async transform(message, controller) {
const result = experiment.inbound(object(message));
await send(result["send"]);
if (result["forward"]) controller.enqueue(result["forward"] as T);
},
}),
),
writable: new WritableStream<T>({
write: (message) => send(experiment.outbound(object(message))),
close: () => writer.close(),
abort: (reason) => writer.abort(reason),
}),
};
}

View File

@@ -1,8 +1,4 @@
import {
RequestError,
type SessionConfigOption,
type SessionNotification,
} from "@agentclientprotocol/sdk/unstable";
import { RequestError, type SessionNotification } from "@agentclientprotocol/sdk/unstable";
import type { AcpSessionNotification, SubagentForm, SubagentState } from "./acp-subagents.js";
import { AIR_SUBAGENT_KEY, airExtensionMeta } from "./air-extension.js";
import { isFinalToolCallStatus } from "./tool-calls/renderer.js";
@@ -68,12 +64,6 @@ export type NativeSubagentSession = {
type Publish = (notification: AcpSessionNotification) => Promise<void>;
type Logger = { log(message: string): void };
type ChildConfiguration = {
options: Map<string, SessionConfigOption>;
published?: string;
publishing: Promise<void>;
};
/** What the SDK says about the end of a task, beyond its status. */
export type TaskEnd = {
/** The SDK's text for a failure. */
@@ -147,7 +137,6 @@ export class NativeSubagentRuntime {
private readonly parentByToolUse: Map<string, string>;
private readonly identityByToolUse = new Map<string, SubagentIdentity>();
private readonly controlByToolUse = new Map<string, AcpSessionNotification>();
private readonly configBySession = new Map<string, ChildConfiguration>();
private readonly childByParentToolUse = new Map<string, NativeSubagent>();
/**
* The child session of each tool call that went to a child session. A later
@@ -227,95 +216,6 @@ export class NativeSubagentRuntime {
return routed;
}
/** Report the model of a child response, not the parent's model or a launch-time alias. */
async reportModel(
parentToolUseId: string | null,
model: string | undefined,
deliver: Publish,
): Promise<void> {
if (this.form !== "rfd" || !parentToolUseId || !model || model === "<synthetic>") {
return;
}
await deliver({
sessionId: this.rootSessionId,
update: {
sessionUpdate: "config_option_update",
configOptions: [
{
id: "model",
name: "Model",
category: "model",
type: "select",
currentValue: model,
options: [{ value: model, name: model }],
},
],
_meta: { claudeCode: { parentToolUseId } },
},
});
}
/** Hook observations can precede the task event that establishes a child's parent. */
async reportConfiguration(
agentId: string,
options: SessionConfigOption[],
meta?: SessionNotification["update"]["_meta"],
): Promise<void> {
if (this.form !== "rfd" || agentId === this.rootSessionId || options.length === 0) return;
let config = this.configBySession.get(agentId);
if (!config) {
const pending = [...this.configBySession.keys()].filter(
(id) => !this.children.has(id),
).length;
if (!this.children.has(agentId) && pending >= MAX_PENDING_PARENTS) {
this.logger.log(
`Session ${this.rootSessionId}: dropping configuration for unannounced subagent ${agentId}; pending buffer limit reached`,
);
return;
}
config = { options: new Map(), publishing: Promise.resolve() };
this.configBySession.set(agentId, config);
}
for (const option of options) config.options.set(option.id, option);
const child = this.children.get(agentId);
if (child?.announced) await this.publishConfiguration(child, meta);
}
private async publishConfiguration(
child: NativeSubagent,
meta?: SessionNotification["update"]["_meta"],
): Promise<void> {
const config = this.configBySession.get(child.sessionId);
if (!config) return;
// SDK hooks can run concurrently with streamed responses. Serialize snapshots
// so a delayed publication cannot replace newer settings with older ones.
const publishing = config.publishing.then(async () => {
if (this.configBySession.get(child.sessionId) !== config) return;
const priority = ["mode", "model", "thought_level", "model_config"];
const rank = (option: SessionConfigOption) => {
const index = priority.indexOf(option.category ?? "");
return index < 0 ? priority.length : index;
};
const configOptions = [...config.options.values()].sort(
(left, right) => rank(left) - rank(right),
);
const snapshot = JSON.stringify(configOptions);
if (config.published === snapshot) return;
await this.publish({
sessionId: child.sessionId,
update: {
sessionUpdate: "config_option_update",
configOptions,
...(meta ? { _meta: meta } : {}),
},
});
config.published = snapshot;
});
// The caller receives the error; later observations must still be able to retry.
config.publishing = publishing.catch(() => {});
await publishing;
}
private async routeUpdate(
notification: AcpSessionNotification,
deliver: Publish,
@@ -416,14 +316,6 @@ export class NativeSubagentRuntime {
? this.childByParentToolUse.get(claudeMeta.parentToolUseId)
: undefined);
if (this.form === "rfd" && announcedChild?.announced) {
if (update.sessionUpdate === "config_option_update") {
await this.reportConfiguration(
announcedChild.sessionId,
update.configOptions,
update._meta,
);
return null;
}
const handback = await this.routeRfdHandback(notification, announcedChild);
if (handback !== undefined) return handback;
}
@@ -703,7 +595,6 @@ export class NativeSubagentRuntime {
this.pendingCount = 0;
this.identityByToolUse.clear();
this.controlByToolUse.clear();
this.configBySession.clear();
this.parentByToolUse.clear();
this.forgetRestores();
}
@@ -1017,7 +908,6 @@ export class NativeSubagentRuntime {
*/
private withdraw(taskId: string, child: NativeSubagent): void {
this.children.delete(taskId);
this.configBySession.delete(child.sessionId);
if (child.parentToolUseId) {
this.takePending(child.parentToolUseId);
this.cleanupControl(child.parentToolUseId);
@@ -1042,7 +932,6 @@ export class NativeSubagentRuntime {
/** Announces `child` in the client's form. A runtime without one never does. */
private async announce(child: NativeSubagent): Promise<void> {
if (this.form) await announceNativeSubagent(child, this.publish, this.form);
await this.publishConfiguration(child);
}
/** The RFD form: publishes the work state of `child` on its parent. */
@@ -1268,7 +1157,6 @@ export class NativeSubagentRuntime {
this.parentByToolUse.clear();
this.identityByToolUse.clear();
this.controlByToolUse.clear();
this.configBySession.clear();
this.childByParentToolUse.clear();
this.taskFinishPromises.clear();
this.senderByToolUse.clear();

View File

@@ -1,3 +1,4 @@
import { backgroundWorkStream } from "./background-work-experiment.js";
import * as v2 from "@agentclientprotocol/sdk/experimental/v2";
import { v1AgentApp, type ClaudeAcpAgent, type Logger } from "./acp-agent.js";
import { nodeToWebReadable, nodeToWebWritable } from "./utils.js";
@@ -37,7 +38,7 @@ export function serveAcp(
agent = created;
};
const { closed } = acpProtocolRouter(logger, onAgent, options.protocols).connect(
v2.ndJsonStream(output, input),
backgroundWorkStream(v2.ndJsonStream(output, input)),
);
return {
closed,

View File

@@ -79,13 +79,6 @@ export function mergeEffortSettings(base: Settings, override: Settings | undefin
};
}
export function effortLevelName(level: string): string {
return level
.split(/[_-]/)
.map((part) => (part ? part.charAt(0).toUpperCase() + part.slice(1) : part))
.join(" ");
}
export function buildEffortConfigOption(
modelInfos: ModelInfo[],
currentModelId: string,
@@ -105,7 +98,10 @@ export function buildEffortConfigOption(
...(useRecommendedValue ? [] : [{ value: "default", name: "Default" }]),
...supportedLevels.map((level) => ({
value: level,
name: effortLevelName(level),
name: level
.split(/[_-]/)
.map((part) => (part ? part.charAt(0).toUpperCase() + part.slice(1) : part))
.join(" "),
})),
];
const includes = (level: string) =>

View File

@@ -292,13 +292,9 @@ function isForkDirectiveMessage(message: { type?: unknown; message?: unknown }):
* The transcript of a `subagent_type: "fork"` child without the context it
* inherited. A fork transcript starts with a copy of the parent's launching
* assistant message (its text, thinking and every tool_use, including the
* Agent call itself), then a user message with a placeholder tool_result for
* each of those tool_uses and the text
* `<fork-boilerplate>…</fork-boilerplate>\nYour directive: <prompt>`. Since
* Claude Code 2.1.307 the fork's own Agent call gets "Fork started — that fork
* is you." and the other tool_uses get "This is the parent's call, not
* yours. …"; before that, every one got "Fork started — processing in
* background". Only the directive text is matched here. A fork
* Agent call itself), then a user message with a "Fork started — processing
* in background" tool_result for each of those tool_uses and the text
* `<fork-boilerplate>…</fork-boilerplate>\nYour directive: <prompt>`. A fork
* launched from inside a subagent also gets a copy of that subagent's history
* before them. None of it is the child's own work: replayed as is, the
* parent's text and tool calls would show again in the child.

View File

@@ -2269,57 +2269,6 @@ describe("usage Markdown", () => {
expect(formatted).not.toContain("lowest");
});
it("renders each window's recommendations in order, escaped, before its MCP table", () => {
const formatted = formatUsageResponse({
...usageResponse,
behaviors: {
...usageResponse.behaviors,
day: {
...usageResponse.behaviors.day,
recommendations: [
{ key: "mcp_server", pct: 41, title: "Disable my_server", detail: "/mcp" },
{ key: "effort", pct: 12, title: "Lower effort | max" },
],
fix: { keys: ["mcp_server"], messages: ["/mcp disable my_server"] },
},
week: {
...usageResponse.behaviors.week,
mcp_servers: [],
recommendations: [{ key: "agent", pct: 5, title: "Trim the reviewer agent" }],
},
},
});
const day = formatted.slice(
formatted.indexOf("**Last 24h**"),
formatted.indexOf("**Last 7d**"),
);
expect(day).toContain(
"- **41%** Disable my\\_server · /mcp\n- **12%** Lower effort \\| max\n\n| MCP server |",
);
expect(formatted).not.toContain("undefined");
expect(formatted).not.toContain("/mcp disable");
expect(
formatted.endsWith(
"**Last 7d** · 43 requests · 5 sessions\n\n- **5%** Trim the reviewer agent",
),
).toBe(true);
});
it("falls back to the CLI text for a malformed recommendation", () => {
expect(
parseUsageResponse({
...usageResponse,
behaviors: {
...usageResponse.behaviors,
day: {
...usageResponse.behaviors.day,
recommendations: [{ key: "effort", pct: "high", title: "Lower effort" }],
},
},
}),
).toBeNull();
});
it("recognizes only exact local usage commands", () => {
expect(isUsageCommandText(" /usage ")).toBe(true);
expect(isUsageCommandText("/cost")).toBe(false);
@@ -3888,8 +3837,7 @@ describe("subagent transcript replay", () => {
it("replays a fork child without the launching message and boilerplate it inherited", async () => {
// A `subagent_type: "fork"` transcript starts with a copy of the
// launching message (same API message id) and a directive message with
// a placeholder result for each of its tool_uses, worded as Claude Code
// 2.1.307+ writes them.
// a "Fork started" result for each of its tool_uses.
const launching = [
{ type: "text", text: "I'll fork a worker." },
toolUse("toolu_fork", "Agent", {
@@ -3902,15 +3850,7 @@ describe("subagent transcript replay", () => {
const forkStarted = (id: string) => ({
type: "tool_result",
tool_use_id: id,
content: [
{
type: "text",
text:
id === "toolu_fork"
? "Fork started — that fork is you."
: "This is the parent's call, not yours. It may not have run yet, and its result goes to the parent.",
},
],
content: [{ type: "text", text: "Fork started — processing in background" }],
});
const trace = await replayRfd(
[
@@ -7237,64 +7177,6 @@ describe("subagent permission attribution (issue #851)", () => {
session_id: "test-session",
});
it("reports the child's observed model as a singleton config without enabling changes", async () => {
const response = (model: string) => ({
type: "assistant",
parent_tool_use_id: "toolu_parent",
uuid: randomUUID(),
session_id: "test-session",
message: {
id: randomUUID(),
role: "assistant",
model,
content: [{ type: "text", text: "Working" }],
usage: { input_tokens: 1, output_tokens: 1 },
},
});
const first = response("child-model");
const { agent, updates } = await runAgent([
{ ...taskStarted("agent-42", "toolu_parent"), subagent_type: "Explore" },
{
type: "stream_event",
parent_tool_use_id: "toolu_parent",
uuid: randomUUID(),
session_id: "test-session",
event: { type: "message_start", message: first.message },
},
first,
response("fallback-model"),
taskUpdated({ status: "completed" }),
]);
const configs = updates.filter(
({ update }) => update.sessionUpdate === "config_option_update",
);
expect(configs).toHaveLength(2);
for (const [index, model] of ["child-model", "fallback-model"].entries()) {
expect(configs[index]).toMatchObject({
sessionId: "agent-42",
update: {
configOptions: [
{
id: "model",
currentValue: model,
options: [{ value: model, name: model }],
},
],
},
});
}
expect(
updates.findIndex(({ update }) => update.sessionUpdate === "subagent_update"),
).toBeLessThan(updates.indexOf(configs[0]!));
await expect(
agent.setSessionConfigOption({
sessionId: "agent-42",
configId: "model",
value: "fallback-model",
}),
).rejects.toThrow("Session not found");
});
it("announces the child as running, then sends it the prompt as a message", async () => {
const updates = await run([
{ ...taskStarted("agent-42", "toolu_parent"), subagent_type: "Explore", prompt: "Go" },
@@ -24281,93 +24163,6 @@ describe("deferred settlement for live background subagents (issues #864/#866)",
]);
});
describe("started by a scheduler fire", () => {
const lifecycleStarted = (fire: { wakeup_source?: string; scheduled_for?: number }) =>
({
type: "command_lifecycle",
command_uuid: "scheduler-fire",
state: "started",
...fire,
uuid: randomUUID(),
session_id: "test-session",
}) as any;
const autonomousTurn = async (fireFrames: (afterRunning: boolean) => any[]) => {
const { agent, asyncTaskUpdates } = await airAgent();
let wake = () => {};
const woken = new Promise<void>((resolve) => (wake = resolve));
injectGeneratorSession(agent, (input) => {
async function* messageGenerator() {
const iter = input[Symbol.asyncIterator]();
yield userEcho((await iter.next()).value);
yield running();
yield resultMessage();
yield idle();
await woken;
yield* fireFrames(false);
yield running();
yield* fireFrames(true);
yield assistantText("woke up");
yield resultMessage();
yield idle();
}
return messageGenerator();
});
await agent.prompt({ sessionId: "test-session", prompt: [{ type: "text", text: "go" }] });
wake();
await agent.sessions["test-session"]?.consumer;
return asyncTaskUpdates()
.filter((update) => update.sessionUpdate !== "async_task_state_update")
.map(({ sessionUpdate, description }) => ({ sessionUpdate, description }));
};
it("names a ScheduleWakeup fire that arrives before the running state", async () => {
const updates = await autonomousTurn((afterRunning) =>
afterRunning
? []
: [
lifecycleStarted({
wakeup_source: "loop_wakeup",
scheduled_for: 1_790_000_000_000,
}),
],
);
expect(updates).toEqual([
{
sessionUpdate: "async_task_spawned",
description: "A scheduled wakeup fired (set for 2026-09-21T14:13:20.000Z).",
},
]);
});
it("renames the open task for a scheduled task fire after the running state", async () => {
const updates = await autonomousTurn((afterRunning) =>
afterRunning ? [lifecycleStarted({ wakeup_source: "schedule_wakeup" })] : [],
);
expect(updates).toEqual([
{
sessionUpdate: "async_task_spawned",
description: "Claude Code started a turn without a prompt.",
},
{ sessionUpdate: "async_task_progress", description: "A scheduled task fired." },
]);
});
it("leaves the task as before for a started frame without a known wakeup source", async () => {
const updates = await autonomousTurn((afterRunning) =>
afterRunning
? [lifecycleStarted({}), lifecycleStarted({ wakeup_source: "future_source" })]
: [],
);
expect(updates).toEqual([
{
sessionUpdate: "async_task_spawned",
description: "Claude Code started a turn without a prompt.",
},
]);
});
});
it("ends the turn task when a prompt is folded into the turn", async () => {
const { agent, updates, asyncTaskUpdates } = await airAgent();
let wake = () => {};

View File

@@ -9,7 +9,6 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"a2","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Review the diff","type":"text"}],"messageId":"toolu_agent","recipientMessageId":"toolu_agent","recipientSessionId":"a2","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"a2","update":{"content":[{"text":"Review the diff","type":"text"}],"messageId":"toolu_agent","recipientSessionId":"a2","senderMessageId":"toolu_agent","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"a2","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_agent"}},"configOptions":[{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","name":"Model","options":[{"name":"claude-sonnet-4-6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"a2","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_agent"}},"content":{"text":"Reviewi","type":"text"},"messageId":"msg_async_sub","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"a2","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_agent"}},"content":{"text":"ng now.","type":"text"},"messageId":"msg_async_sub","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"sessionId":"a2","sessionUpdate":"subagent_update","state":{"state":"idle","stopReason":"end_turn"}}}}

View File

@@ -9,7 +9,6 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Check logs","type":"text"}],"messageId":"toolu_late","recipientMessageId":"toolu_late","recipientSessionId":"agent_late","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"content":[{"text":"Check logs","type":"text"}],"messageId":"toolu_late","recipientSessionId":"agent_late","senderMessageId":"toolu_late","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_late"}},"configOptions":[{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","name":"Model","options":[{"name":"claude-sonnet-4-6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_late"}},"content":[],"kind":"read","locations":[],"name":"Read","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Read File","toolCallId":"toolu_late_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_late","update":{"locations":[{"line":1,"path":"/var/log/app.log"}],"rawInput":{"file_path":"/var/log/app.log"},"sessionUpdate":"tool_call_update","title":"Read /var/log/app.log","toolCallId":"toolu_late_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"The logs are clean.","type":"text"}],"messageId":"toolu_late:reply","recipientSessionId":"<session>","senderSessionId":"agent_late","sessionUpdate":"session_message"}}}

View File

@@ -9,7 +9,6 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Make the build pass","type":"text"}],"messageId":"toolu_native","recipientMessageId":"toolu_native","recipientSessionId":"agent_n","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"content":[{"text":"Make the build pass","type":"text"}],"messageId":"toolu_native","recipientSessionId":"agent_n","senderMessageId":"toolu_native","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"configOptions":[{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","name":"Model","options":[{"name":"claude-sonnet-4-6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"content":{"text":"Running t","type":"text"},"messageId":"msg_native_sub_1","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"content":{"text":"he build.","type":"text"},"messageId":"msg_native_sub_1","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_n","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_native"}},"content":[],"kind":"search","name":"Grep","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"grep","toolCallId":"toolu_native_grep"}}}

View File

@@ -9,12 +9,10 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Plan it","type":"text"}],"messageId":"toolu_outer","recipientMessageId":"toolu_outer","recipientSessionId":"agent_outer","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"content":[{"text":"Plan it","type":"text"}],"messageId":"toolu_outer","recipientSessionId":"agent_outer","senderMessageId":"toolu_outer","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_outer"}},"configOptions":[{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","name":"Model","options":[{"name":"claude-sonnet-4-6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"capabilities":{"cancel":{}},"description":"Read the spec","sessionId":"agent_inner","sessionUpdate":"subagent_update","state":{"state":"running"},"title":"Explore"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_outer","update":{"content":[{"text":"Read spec.md","type":"text"}],"messageId":"toolu_inner","recipientMessageId":"toolu_inner","recipientSessionId":"agent_inner","senderSessionId":"agent_outer","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"content":[{"text":"Read spec.md","type":"text"}],"messageId":"toolu_inner","recipientSessionId":"agent_inner","senderMessageId":"toolu_inner","senderSessionId":"agent_outer","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_inner"}},"configOptions":[{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","name":"Model","options":[{"name":"claude-sonnet-4-6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_inner"}},"content":[],"kind":"read","locations":[],"name":"Read","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Read File","toolCallId":"toolu_grandchild_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"locations":[{"line":1,"path":"/spec.md"}],"rawInput":{"file_path":"/spec.md"},"sessionUpdate":"tool_call_update","title":"Read /spec.md","toolCallId":"toolu_grandchild_read"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_inner","update":{"content":[{"content":{"text":"```\n1\tspec\n```","type":"text"},"type":"content"}],"rawOutput":"1\tspec","sessionUpdate":"tool_call_update","status":"completed","toolCallId":"toolu_grandchild_read"}}}

View File

@@ -9,7 +9,6 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Find the parser","type":"text"}],"messageId":"toolu_task","recipientMessageId":"toolu_task","recipientSessionId":"agent_1","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"content":[{"text":"Find the parser","type":"text"}],"messageId":"toolu_task","recipientSessionId":"agent_1","senderMessageId":"toolu_task","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"configOptions":[{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","name":"Model","options":[{"name":"claude-sonnet-4-6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":{"text":"Looking for","type":"text"},"messageId":"msg_sub_1","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":{"text":" the parser.","type":"text"},"messageId":"msg_sub_1","sessionUpdate":"agent_message_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_1","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_task"}},"content":[],"kind":"read","locations":[],"name":"Read","rawInput":{},"sessionUpdate":"tool_call_update","status":"pending","title":"Read File","toolCallId":"toolu_sub_read"}}}

View File

@@ -9,7 +9,6 @@
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"sessionUpdate":"state_update","state":"running"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"<session>","update":{"content":[{"text":"Summarize the repo","type":"text"}],"messageId":"toolu_transcript","recipientMessageId":"toolu_transcript","recipientSessionId":"agent_t","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"content":[{"text":"Summarize the repo","type":"text"}],"messageId":"toolu_transcript","recipientSessionId":"agent_t","senderMessageId":"toolu_transcript","senderSessionId":"<session>","sessionUpdate":"session_message"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"configOptions":[{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","name":"Model","options":[{"name":"claude-sonnet-4-6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionUpdate":"config_option_update"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"content":{"text":"Look at t","type":"text"},"messageId":"msg_transcript_sub","sessionUpdate":"agent_thought_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"content":{"text":"he layout.","type":"text"},"messageId":"msg_transcript_sub","sessionUpdate":"agent_thought_chunk"}}}
{"kind":"notification","method":"session/update","payload":{"sessionId":"agent_t","update":{"_meta":{"claudeCode":{"parentToolUseId":"toolu_transcript"}},"content":{"text":"The repo has ","type":"text"},"messageId":"msg_transcript_sub","sessionUpdate":"agent_message_chunk"}}}

View File

@@ -0,0 +1,244 @@
import { describe, expect, it } from "vitest";
import { BackgroundWorkExperiment, backgroundWorkStream } from "../background-work-experiment.js";
const update = (value: Record<string, unknown>, sessionId = "s") => ({
method: "session/update",
params: { sessionId, update: value },
});
const level = (taskIds: unknown) =>
update({
sessionUpdate: "session_info_update",
_meta: { backgroundWorkExperiment: { taskIds } },
});
const expected = (background: unknown, state = "idle") => ({
params: {
sessionId: "s",
update: { sessionUpdate: "state_update", state, background },
},
});
describe.each([1, 2])("background work experiment v%s", (version) => {
function fixture() {
const bridge = new BackgroundWorkExperiment();
bridge.inbound({
id: 1,
method: "initialize",
params: { protocolVersion: version },
});
bridge.inbound({ id: 2, method: "session/new", params: {} });
bridge.outbound({ id: 2, result: { sessionId: "s" } });
return bridge;
}
function spawn(bridge: BackgroundWorkExperiment) {
bridge.outbound(
update({
sessionUpdate: version === 1 ? "tool_call" : "tool_call_update",
toolCallId: "tool",
status: "in_progress",
}),
);
bridge.outbound(level(["native"]));
return bridge.outbound(
update({
sessionUpdate: version === 1 ? "async_task_spawned" : "_async_task_spawned",
asyncTaskId: "native",
taskType: "shell",
toolCallId: "tool",
canStop: true,
}),
);
}
it("does not infer empty from a silent backend or unmapped monitor", () => {
const bridge = fixture();
expect(bridge.snapshot("s")).toMatchObject(expected(null));
expect(bridge.outbound(level(["monitor"]))).toMatchObject([expected(null)]);
expect(bridge.outbound(level([]))).toMatchObject([expected([])]);
expect(bridge.outbound(level([null]))).toMatchObject([expected(null)]);
expect(bridge.outbound(level("damaged"))).toMatchObject([expected(null)]);
});
it("references announced tools and keeps them running after foreground completion", () => {
const bridge = fixture();
expect(spawn(bridge)).toMatchObject([
{
params: {
update: {
sessionUpdate: "tool_call_update",
toolCallId: "tool",
status: "in_progress",
capabilities: { cancel: {} },
},
},
},
expected([{ type: "tool_call", toolCallId: "tool" }]),
]);
expect(
bridge.outbound(
update({
sessionUpdate: "tool_call_update",
toolCallId: "tool",
status: "completed",
}),
),
).toMatchObject([{ params: { update: { status: "in_progress" } } }]);
bridge.outbound(
update({
sessionUpdate: "state_update",
state: "idle",
stopReason: "end_turn",
}),
);
expect(bridge.outbound(level(["native"]))).toMatchObject([
expected([{ type: "tool_call", toolCallId: "tool" }]),
]);
expect(bridge.outbound(level(["native"]))[0]).not.toHaveProperty("params.update.stopReason");
bridge.inbound({
method: "session/cancel",
params: { sessionId: "s" },
});
expect(bridge.snapshot("s")).toMatchObject(
expected([{ type: "tool_call", toolCallId: "tool" }]),
);
});
it("acceptance does not finalize, and completion wins a cancellation race", () => {
const bridge = fixture();
spawn(bridge);
expect(
bridge.inbound({
id: 3,
method: "tool_call/cancel",
params: { sessionId: "s", toolCallId: "tool" },
}),
).toMatchObject({
forward: {
method: "_session/async_task/stop",
params: { asyncTaskId: "native" },
},
});
expect(bridge.outbound({ id: 3, result: { stopped: true } })).toMatchObject([{ result: {} }]);
expect(bridge.snapshot("s")).toMatchObject(
expected([{ type: "tool_call", toolCallId: "tool" }]),
);
expect(
bridge.outbound(
update({
sessionUpdate: "async_task_state_update",
asyncTaskId: "native",
state: "completed",
}),
),
).toMatchObject([
{ params: { update: { status: "completed", capabilities: null } } },
expected([]),
]);
expect(
bridge.outbound(
update({
sessionUpdate: "async_task_state_update",
asyncTaskId: "native",
state: "stopped",
}),
),
).toMatchObject([expected([])]);
expect(
bridge.inbound({
id: 4,
method: "tool_call/cancel",
params: { sessionId: "s", toolCallId: "tool" },
}),
).toMatchObject({ send: [{ error: { code: -32602 } }] });
});
it("does not revive a tool finalized before late native discovery", () => {
const bridge = fixture();
bridge.outbound(
update({
sessionUpdate: version === 1 ? "tool_call" : "tool_call_update",
toolCallId: "late",
status: "completed",
}),
);
bridge.outbound(level(["native"]));
const messages = bridge.outbound(
update({
sessionUpdate: "async_task_spawned",
asyncTaskId: "native",
taskType: "shell",
toolCallId: "late",
canStop: true,
}),
);
expect(messages).toContainEqual(expect.objectContaining(expected(null)));
expect(messages).not.toContainEqual(
expect.objectContaining({
params: expect.objectContaining({
update: expect.objectContaining({ status: "in_progress" }),
}),
}),
);
expect(
bridge.inbound({
id: 30,
method: "tool_call/cancel",
params: { sessionId: "s", toolCallId: "late" },
}),
).toMatchObject({ send: [{ error: { code: -32602 } }] });
});
it("does not claim scheduled workflows even when they name an announced tool", () => {
const bridge = fixture();
bridge.outbound(
update({
sessionUpdate: version === 1 ? "tool_call" : "tool_call_update",
toolCallId: "scheduled",
status: "pending",
}),
);
bridge.outbound(level(["workflow"]));
const messages = bridge.outbound(
update({
sessionUpdate: "async_task_spawned",
asyncTaskId: "workflow",
taskType: "workflow",
toolCallId: "scheduled",
canStop: true,
}),
);
expect(messages.at(-1)).toMatchObject(expected(null));
expect(
bridge.inbound({
id: 31,
method: "tool_call/cancel",
params: { sessionId: "s", toolCallId: "scheduled" },
}),
).toMatchObject({ send: [{ error: { code: -32602 } }] });
expect(
bridge.outbound(
update({ sessionUpdate: "tool_call_update", toolCallId: "scheduled", status: "pending" }),
)[0],
).toMatchObject({ params: { update: { status: "pending" } } });
});
it("does not fabricate referenced tools during resume and starts forks empty", () => {
const bridge = fixture();
bridge.inbound({
id: 8,
method: "session/resume",
params: { sessionId: "restored" },
});
expect(bridge.outbound({ id: 8, result: {} })[0]).toMatchObject({
params: { sessionId: "restored", update: { background: null } },
});
bridge.inbound({
id: 9,
method: "session/fork",
params: { sessionId: "s" },
});
expect(bridge.outbound({ id: 9, result: { sessionId: "fork" } })[1]).toMatchObject({
params: { sessionId: "fork", update: { background: [] } },
});
});
it("leaves the stream untouched when disabled", () => {
const stream = {
readable: new ReadableStream(),
writable: new WritableStream(),
};
expect(backgroundWorkStream(stream)).toBe(stream);
});
});

View File

@@ -7,9 +7,7 @@ import {
import type { ClientCapabilities } from "@agentclientprotocol/sdk/unstable";
import { getSessionMessages, type Options } from "@anthropic-ai/claude-agent-sdk";
import type { AcpClient, ClaudeAcpAgent as ClaudeAcpAgentType } from "../acp-agent.js";
import type { AcpSessionNotification } from "../acp-subagents.js";
import { ALLOW_BYPASS } from "../permissions/modes.js";
import { NativeSubagentRuntime } from "../native-subagents.js";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
@@ -187,122 +185,6 @@ describe("createSession options merging", () => {
]);
});
it.each([false, true])("reports child settings through registered hooks (v2: %s)", async (v2) => {
const updates: AcpSessionNotification[] = [];
const client = createMockClient();
client.sessionUpdate = async (notification) => {
updates.push(notification);
};
const observedAgent = new ClaudeAcpAgent(client, undefined, { v2 });
await observedAgent.initialize({ protocolVersion: 1, clientCapabilities: { subagents: {} } });
const { sessionId } = await observedAgent.newSession({ cwd: process.cwd(), mcpServers: [] });
const session = observedAgent.sessions[sessionId]!;
const parentOptions = structuredClone(session.configOptions);
const runtime = (session.nativeSubagentRuntime = new NativeSubagentRuntime(
"rfd",
sessionId,
session,
(notification) => client.sessionUpdate(notification),
{ log: () => {} },
));
const invoke = async (
event: "PreToolUse" | "PostToolUse" | "PostModelSwitch" | "SubagentStart" | "SubagentStop",
fields: Record<string, unknown>,
) => {
const matcher = capturedOptions!.hooks![event]!.find((matcher) => !matcher.matcher);
expect(matcher).toBeDefined();
for (const hook of matcher!.hooks) {
await expect(
hook(
{
hook_event_name: event,
session_id: sessionId,
cwd: process.cwd(),
transcript_path: "/tmp/transcript",
...fields,
} as any,
undefined,
{ signal: new AbortController().signal },
),
).resolves.toMatchObject({ continue: true });
}
};
// A main-thread --agent persona has agent_type too, but no agent_id.
await invoke("PreToolUse", {
agent_type: "reviewer",
permission_mode: "plan",
effort: { level: "max" },
});
expect(updates).toEqual([]);
await invoke("SubagentStart", {
agent_id: "child",
agent_type: "Explore",
permission_mode: "acceptEdits",
});
expect(updates).toEqual([]);
await runtime.taskStarted({ taskId: "child", subagentType: "Explore" }, async () => {});
const configs = () =>
updates.flatMap(({ update }) =>
update.sessionUpdate === "config_option_update" ? [update.configOptions] : [],
);
expect(
configs()
.at(-1)
?.map((option) => [option.id, option.currentValue]),
).toEqual([
["mode", "acceptEdits"],
["agent", "Explore"],
]);
await invoke("PreToolUse", {
agent_id: "child",
agent_type: "Explore",
effort: { level: "high" },
});
expect(
configs()
.at(-1)
?.find((option) => option.id === "effort"),
).toMatchObject({
category: "thought_level",
currentValue: "high",
options: [{ value: "high", name: "High" }],
});
await invoke("PostToolUse", {
agent_id: "child",
tool_name: "EnterPlanMode",
permission_mode: "plan",
});
await invoke("PostModelSwitch", {
agent_id: "child",
to_model: "child-model",
source: "auto",
effort: { level: "low" },
});
await invoke("SubagentStop", { agent_id: "child", agent_type: "Explore" });
expect(
configs()
.at(-1)
?.map((option) => [option.id, option.currentValue]),
).toEqual([
["mode", "plan"],
["model", "child-model"],
["effort", "low"],
["agent", "Explore"],
]);
expect(
configs()
.at(-1)
?.every((option) => option.type === "select" && option.options.length === 1),
).toBe(true);
await new Promise((resolve) => setImmediate(resolve));
expect(session.configOptions).toEqual(parentOptions);
expect(
updates
.filter(({ update }) => update.sessionUpdate === "config_option_update")
.every(({ sessionId: target }) => target === "child"),
).toBe(true);
});
it("merges user-provided disallowedTools with ACP internal list", async () => {
await agent.newSession({
cwd: process.cwd(),

View File

@@ -842,221 +842,6 @@ describe("NativeSubagentRuntime in the RFD form", () => {
update.sessionUpdate === "subagent_update" && update.state ? [update.state] : [],
);
it("reports only the observed child model and refreshes it on a model change", async () => {
const { runtime, published } = await runningWorker({ model: "inherit" });
const deliver = async (notification: AcpSessionNotification) => {
const routed = await runtime.route(notification, deliver);
if (routed) published.push(routed);
};
await runtime.reportModel(null, "parent-model", deliver);
await runtime.reportModel("launch-1", "<synthetic>", deliver);
expect(published.some(({ update }) => update.sessionUpdate === "config_option_update")).toBe(
false,
);
await runtime.reportModel("launch-1", "child-model", deliver);
await runtime.reportModel("launch-1", "child-model", deliver);
await runtime.finishTask("worker-1", "completed", deliver, "launch-1");
await runtime.taskStarted(
{ taskId: "worker-1", toolUseId: "resume-1", prompt: "Continue" },
deliver,
);
await runtime.reportModel("resume-1", "fallback-model", deliver);
const configs = published.filter(
({ update }) => update.sessionUpdate === "config_option_update",
);
expect(configs).toHaveLength(2);
for (const [index, model] of ["child-model", "fallback-model"].entries()) {
expect(configs[index]).toMatchObject({
sessionId: "worker-1",
update: {
sessionUpdate: "config_option_update",
configOptions: [
{
id: "model",
category: "model",
type: "select",
currentValue: model,
options: [{ value: model, name: model }],
},
],
},
});
}
});
it("buffers model observations until the child is announced", async () => {
const published: AcpSessionNotification[] = [];
const runtime = new NativeSubagentRuntime(
"rfd",
"root",
{},
async (notification) => {
published.push(notification);
},
{ log: () => {} },
);
const deliver = async (notification: AcpSessionNotification) => {
const routed = await runtime.route(notification, deliver);
if (routed) published.push(routed);
};
await runtime.reportModel("launch-1", "child-model", deliver);
expect(published).toEqual([]);
await runtime.taskStarted(
{ taskId: "worker-1", toolUseId: "launch-1", subagentType: "Explore" },
deliver,
);
expect(published).toEqual([]);
await deliver(launch("launch-1", {}));
expect(published.map(({ update }) => update.sessionUpdate)).toEqual([
"subagent_update",
"config_option_update",
]);
expect(published[1]?.sessionId).toBe("worker-1");
});
const effortOption = (level: string) => ({
id: "effort",
name: "Effort",
category: "thought_level",
type: "select" as const,
currentValue: level,
options: [{ value: level, name: level }],
});
it("merges hook observations with model snapshots without inheriting other sessions", async () => {
const { runtime, published } = await runningWorker();
const deliver = async (notification: AcpSessionNotification) => {
const routed = await runtime.route(notification, deliver);
if (routed) published.push(routed);
};
await runtime.reportConfiguration("worker-1", [
effortOption("high"),
{
id: "mode",
name: "Mode",
category: "mode",
type: "select",
currentValue: "plan",
options: [{ value: "plan", name: "Plan" }],
},
]);
await runtime.reportModel("launch-1", "child-model", deliver);
await runtime.reportConfiguration("worker-1", [effortOption("low")]);
await runtime.reportModel("launch-1", "fallback-model", deliver);
const configs = published.flatMap(({ update }) =>
update.sessionUpdate === "config_option_update" ? [update.configOptions] : [],
);
expect(configs).toHaveLength(4);
expect(configs.at(-1)?.map((option) => [option.id, option.currentValue])).toEqual([
["mode", "plan"],
["model", "fallback-model"],
["effort", "low"],
]);
expect(
configs.at(-1)?.every((option) => option.type === "select" && option.options.length === 1),
).toBe(true);
await runtime.reportConfiguration("root", [effortOption("max")]);
expect(
published.filter(({ update }) => update.sessionUpdate === "config_option_update"),
).toHaveLength(4);
});
it("keeps hook observations pending until truthful parentage is announced", async () => {
const published: AcpSessionNotification[] = [];
const runtime = new NativeSubagentRuntime(
"rfd",
"root",
{},
async (notification) => {
published.push(notification);
},
{ log: () => {} },
);
await runtime.reportConfiguration("worker-1", [effortOption("high")]);
expect(published).toEqual([]);
await runtime.taskStarted(
{ taskId: "worker-1", toolUseId: "launch-1", subagentType: "Explore" },
async () => {},
);
expect(published).toEqual([]);
await runtime.route(launch("launch-1", {}), async () => {});
expect(published.map(({ sessionId, update }) => [sessionId, update.sessionUpdate])).toEqual([
["root", "subagent_update"],
["worker-1", "config_option_update"],
]);
});
it("serializes concurrent config publications and retries a failed snapshot", async () => {
const started = deferred();
const release = deferred();
const published: AcpSessionNotification[] = [];
const value = child({ announced: true });
let fail = false;
const runtime = new NativeSubagentRuntime(
"rfd",
"root",
sessionWith(value),
async (notification) => {
if (fail) throw new Error("transport failed");
published.push(notification);
if (published.length === 1) {
started.resolve();
await release.promise;
}
},
{ log: () => {} },
);
const first = runtime.reportConfiguration(value.sessionId, [effortOption("high")]);
await started.promise;
const second = runtime.reportConfiguration(value.sessionId, [effortOption("low")]);
release.resolve();
await Promise.all([first, second]);
expect(
published.flatMap(({ update }) =>
update.sessionUpdate === "config_option_update"
? update.configOptions.map((option) => option.currentValue)
: [],
),
).toEqual(["high", "low"]);
fail = true;
await expect(
runtime.reportConfiguration(value.sessionId, [effortOption("max")]),
).rejects.toThrow("transport failed");
fail = false;
await runtime.reportConfiguration(value.sessionId, [effortOption("max")]);
expect(published).toHaveLength(3);
});
it("forgets unannounced configuration when the runtime is cleared", async () => {
const published: AcpSessionNotification[] = [];
const runtime = new NativeSubagentRuntime(
"rfd",
"root",
{},
async (notification) => {
published.push(notification);
},
{ log: () => {} },
);
await runtime.reportConfiguration("worker-1", [effortOption("high")]);
runtime.clear();
await runtime.taskStarted({ taskId: "worker-1", subagentType: "Explore" }, async () => {});
expect(published.map(({ update }) => update.sessionUpdate)).toEqual(["subagent_update"]);
});
it.each(["air", undefined] as const)(
"does not report child config in the %s form",
async (form) => {
const deliver = vi.fn(async () => {});
const runtime = new NativeSubagentRuntime(form, "root", {}, deliver, { log: () => {} });
await runtime.reportModel("launch-1", "child-model", deliver);
await runtime.reportConfiguration("worker-1", [effortOption("high")]);
expect(deliver).not.toHaveBeenCalled();
},
);
it("hides the Agent tool call, which the parent's session messages replace", async () => {
const { runtime, control } = await runningWorker();

View File

@@ -41,8 +41,7 @@ const launch = (uuid: string, launchId: string) => ({
});
/**
* The directive message that Claude Code writes after the copy of the launching
* message in a `subagent_type: "fork"` child's transcript, with the tool_result
* text of Claude Code before 2.1.307.
* message in a `subagent_type: "fork"` child's transcript.
*/
const forkDirective = (uuid: string, launchIds: string[], prompt: string) => ({
...base,

View File

@@ -431,7 +431,7 @@ export const createPostToolUseHook =
async (input: any, toolUseID: string | undefined): Promise<{ continue: boolean }> => {
if (input.hook_event_name === "PostToolUse") {
// Handle EnterPlanMode tool - notify client of mode change after successful execution
if (input.tool_name === "EnterPlanMode" && !input.agent_id && options?.onEnterPlanMode) {
if (input.tool_name === "EnterPlanMode" && options?.onEnterPlanMode) {
await options.onEnterPlanMode();
}

View File

@@ -8,18 +8,10 @@ const usageWindowSchema = z
.nullable()
.optional();
const contributionSchema = z.object({ name: z.string(), pct: percentSchema });
// Claude Code 2.1.305+ suggests one change per notable source, largest share first.
const recommendationSchema = z.object({
key: z.string(),
pct: percentSchema,
title: z.string(),
detail: z.string().optional(),
});
const behaviorPeriodSchema = z.object({
request_count: countSchema,
session_count: countSchema,
mcp_servers: z.array(contributionSchema),
recommendations: z.array(recommendationSchema).optional(),
});
const modelUsageSchema = z.object({
inputTokens: countSchema,
@@ -141,23 +133,12 @@ function appendContributions(
request_count: number;
session_count: number;
mcp_servers: { name: string; pct: number }[];
recommendations?: { pct: number; title: string; detail?: string }[];
},
): void {
lines.push(
"",
`**${label}** · ${period.request_count} requests · ${period.session_count} sessions`,
);
// Rendered in the order Claude Code sends them, as its own `/usage` does. The
// window's `fix` is left out: its messages must be queued at priority "later"
// with `client_composed` set, which an ACP prompt cannot express.
if (period.recommendations?.length) {
lines.push("");
for (const recommendation of period.recommendations) {
const detail = recommendation.detail ? ` · ${escapeMarkdown(recommendation.detail)}` : "";
lines.push(`- **${recommendation.pct}%** ${escapeMarkdown(recommendation.title)}${detail}`);
}
}
if (period.mcp_servers.length === 0) return;
lines.push("", "| MCP server | Usage |", "|:--|--:|");
for (const server of [...period.mcp_servers].sort((a, b) => b.pct - a.pct).slice(0, 3)) {

View File

@@ -14,6 +14,7 @@ import type {
RequestPermissionResponse,
} from "@agentclientprotocol/sdk/unstable";
import * as v2 from "@agentclientprotocol/sdk/experimental/v2/unstable";
import { z } from "zod";
import { ClaudeAcpAgent, type AcpClient, type Logger } from "../acp-agent.js";
import type { AcpSessionNotification } from "../acp-subagents.js";
import type { AcpPermissionRequest } from "../permissions/presentation.js";
@@ -129,7 +130,16 @@ export function v2AgentApp(
void client.send({ sessionId: params.sessionId, update });
}),
)
.onNotification(v2.methods.agent.session.cancel, ({ params }) => agent.cancel(params));
.onNotification(v2.methods.agent.session.cancel, ({ params }) => agent.cancel(params))
.onRequest(
"_session/async_task/stop",
z.object({ sessionId: z.string(), asyncTaskId: z.string() }),
({ params }) => {
if (process.env.ACP_BACKGROUND_WORK_EXPERIMENT !== "1")
throw new Error("Background work experiment is disabled");
return agent.stopAsyncTask(params);
},
);
}
/** The `state_update` that reports `state` as the current foreground state. */

View File

@@ -76,6 +76,15 @@ export function v2SessionUpdate(
// `configOptions` of a `session/set_config_option` response.
return undefined;
default:
if (
process.env.ACP_BACKGROUND_WORK_EXPERIMENT === "1" &&
update.sessionUpdate.startsWith("async_task_")
) {
return {
...update,
sessionUpdate: `_${update.sessionUpdate}`,
} as unknown as v2.SessionUpdate;
}
throw new Error(
`The ACP v2 surface does not translate ${update.sessionUpdate} session updates yet`,
);