feat: update to Claude Code 2.1.309 (#1413)

Updates `@anthropic-ai/claude-agent-sdk` from 0.3.303 to **0.3.309**
(Claude Code **2.1.309**), `@agentclientprotocol/sdk` 2.0.0 → 2.0.1,
`@anthropic-ai/sdk` 0.135.0 → 1.1.0 and typescript-eslint 8.71.2 →
8.72.0.

TypeScript stays on 6.0.3. typescript-eslint requires `typescript
<6.1.0` and has no TS 7 support yet
(typescript-eslint/typescript-eslint#10940), so we wait for that rather
than running two TypeScript versions side by side.

## Uses new SDK features

- **`/usage` recommendations (0.3.305):** our structured `/usage`
Markdown now lists Claude Code's per-window suggestions (`- **41%**
Disable my_server · /mcp`) above the MCP server table, in the order
Claude Code sends them. `fix` is not rendered: its messages must be
queued at priority `later` with `client_composed` set, which an ACP
prompt cannot express. A malformed recommendation makes the response
fail validation, so we fall back to Claude Code's own `/usage` text.
- **`wakeup_source` on `command_lifecycle` "started" (0.3.307):** when a
ScheduleWakeup or another scheduled task starts a turn with no prompt
open, the async-task (AIR) "Claude is working" task now says `A
scheduled wakeup fired (set for <UTC time>).` or `A scheduled task
fired.` instead of `Claude Code started a turn without a prompt.` It
works whether the frame arrives before or after `session_state_changed:
running`; older CLIs and unknown values behave as before.

## Fixes for changes in the new CLI

- **Fork transcripts (2.1.307):** the placeholder `tool_result` text in
a fork's copied prefix changed (`Fork started — that fork is you.` /
`This is the parent's call, not yours. …`). `stripForkPrefix` matches
only the directive text, so behaviour is unchanged; its doc comment and
the replay fixture now describe both formats.

## Behaviour changes ACP users may notice

- **2.1.304:** tool search is on by default behind a custom
`ANTHROPIC_BASE_URL` (our `providers/set` gateway sessions), and a
gateway that rejects it is retried without it (2.1.305).
- **2.1.304:** `rm -r` of a mounted drive or a top-level folder on it
(`/Volumes/…`, `/media/…`, `/mnt/c/…`) now asks for permission, even in
bypass mode, without an always-allow option.
`CLAUDE_CODE_DISABLE_MOUNTED_DRIVE_RM_PROMPT` turns it off.
- **2.1.309:** with `sandbox.enabled` but no runnable sandbox, commands
in `sandbox.excludedCommands` now ask before writing to the sandbox's
write-list paths, and the unsandboxed fallback is refused.
- **2.1.304:** `allowedMcpServers` no longer filters `managed-mcp.json`
stdio servers that use `${VAR}` only in `env`.

## Upstream fixes this picks up

- Model switches, prompts and interrupts no longer wait behind our
background `getContextUsage` call (2.1.304); `setModel()` waits up to 15
s for the API to confirm a model (2.1.307).
- A PreToolUse hook's deny reason is no longer replaced by a timeout of
our `Edit|Write` preview callback hook (2.1.304).
- Host-supplied `managedSettings` no longer drop `disableAllHooks`,
`disableSkillShellExecution` and empty HTTP hook lists (0.3.309).
- Gateway fixes: `context_management` rejections are retried without the
field (2.1.304); no `prompt-caching-scope` header behind custom gateways
(2.1.307).
- A disabled MCP server is no longer reconnected by a later permission
prompt in SDK sessions; HTTP MCP tool calls are no longer sent twice
when the connection drops (2.1.307/2.1.309).
- Bash permission and sandbox hardening (2.1.309), fewer "prompt is too
long" and autocompact-thrashing errors, and faster resume of large
compacted sessions.

## Checked, no change needed

- **ACP SDK 2.0.1** only changes the client-side `createHttpStream`
transport, which we don't use.
- **`@anthropic-ai/sdk` 1.x** (API version 2026-10-21, Bearer auth,
sampling params removed): we only use its types, none of which changed
in a way that affects us. Neither our `dist/` nor the Agent SDK runtime
imports it, and the bundled CLI carries its own client. It is Apache-2.0
from 1.0.0 (previously MIT).
- **`sed -i` (2.1.309)** runs as plain Bash; SDK sessions already did,
since `_simulatedSedEdit` only exists in the TUI preview.
- The remaining type changes (`liveReply`, context-usage row `id`s,
`pluginId`/`path` on agents and skills, new settings keys) are additive
and unused.

Verified: `npm run build`, `npm run check`, `npm run test:run` (3277
passed, 30 skipped), and a typecheck of `examples/`.
This commit is contained in:
Ben Brandt
2026-10-23 21:43:02 +02:00
committed by GitHub
parent 0fe22ae5a1
commit a1521b6196
8 changed files with 353 additions and 118 deletions

214
package-lock.json generated
View File

@@ -9,8 +9,8 @@
"version": "0.94.0",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "2.0.0",
"@anthropic-ai/claude-agent-sdk": "0.3.303",
"@agentclientprotocol/sdk": "2.0.1",
"@anthropic-ai/claude-agent-sdk": "0.3.309",
"diff": "9.0.0",
"zod": "4.6.7"
},
@@ -18,12 +18,12 @@
"claude-agent-acp": "dist/index.js"
},
"devDependencies": {
"@anthropic-ai/sdk": "0.135.0",
"@anthropic-ai/sdk": "1.1.0",
"@eslint/js": "10.0.1",
"@tsconfig/node22": "22.0.6",
"@types/node": "26.6.5",
"@typescript-eslint/eslint-plugin": "8.71.2",
"@typescript-eslint/parser": "8.71.2",
"@typescript-eslint/eslint-plugin": "8.72.0",
"@typescript-eslint/parser": "8.72.0",
"ajv": "8.20.0",
"ajv-formats": "3.0.1",
"eslint": "10.13.0",
@@ -39,31 +39,31 @@
}
},
"node_modules/@agentclientprotocol/sdk": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-2.0.0.tgz",
"integrity": "sha512-Za5gWOPQ44aBOdegDJZ+OjCAPn7POc0cw8BlFJQ1mmUY1OoUWxRi3+QZT4gdZnyEWgKXyfeGRb0Oo1ag9YkQ1w==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-2.0.1.tgz",
"integrity": "sha512-v4E/NcBTGFEKmorGso8ysAjJTkNh5UMJPP4aDznyMNmnP9t10g/aCCo/Djy7RYaYZy5ZROFK3G5cEj9tDgGOTQ==",
"license": "Apache-2.0",
"peerDependencies": {
"zod": "^3.25.76 || ^4.0.0"
}
},
"node_modules/@anthropic-ai/claude-agent-sdk": {
"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==",
"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==",
"license": "SEE LICENSE IN README.md",
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"@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"
"@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"
},
"peerDependencies": {
"@anthropic-ai/sdk": ">=0.93.0",
@@ -72,9 +72,9 @@
}
},
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": {
"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==",
"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==",
"cpu": [
"arm64"
],
@@ -85,9 +85,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": {
"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==",
"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==",
"cpu": [
"x64"
],
@@ -98,12 +98,15 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": {
"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==",
"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==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
@@ -111,12 +114,15 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": {
"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==",
"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==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
@@ -124,12 +130,15 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": {
"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==",
"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==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
@@ -137,12 +146,15 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": {
"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==",
"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==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
@@ -150,9 +162,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": {
"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==",
"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==",
"cpu": [
"arm64"
],
@@ -163,9 +175,9 @@
]
},
"node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": {
"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==",
"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==",
"cpu": [
"x64"
],
@@ -176,10 +188,10 @@
]
},
"node_modules/@anthropic-ai/sdk": {
"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",
"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",
"dependencies": {
"@opentelemetry/api": "^1.4.1",
"json-schema-to-ts": "^3.1.1",
@@ -919,17 +931,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"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==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@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",
"@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",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -942,22 +954,22 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.71.2",
"@typescript-eslint/parser": "^8.72.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.2.tgz",
"integrity": "sha512-aTtmZjp/x3gsGHkDs04/1I8kNogBJkpQaeGaN0PfiG43NzDEmW2n+OP5im9wfsvyX0yP1UZWBrAGtDBfH9jEVw==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@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",
"@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",
"debug": "^4.4.3"
},
"engines": {
@@ -973,14 +985,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.2.tgz",
"integrity": "sha512-Pd68GsLVh5ZXlcEccFPdhSOvtHZ/2WU0XsA8zmXOS3F158lVlcTLhfQ6qvtL1RzbMie0Kp8xtbVwAg4nejKPHw==",
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.0.tgz",
"integrity": "sha512-bzHngBbmIgzj6TsTjVUCntGx6FFLgE7mPjpO0PzHycOaZHXqS43XnrkFIHRS16LLr/xO6FHSXG3GJFo/F4bSBQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.71.2",
"@typescript-eslint/types": "^8.71.2",
"@typescript-eslint/tsconfig-utils": "^8.72.0",
"@typescript-eslint/types": "^8.72.0",
"debug": "^4.4.3"
},
"engines": {
@@ -995,14 +1007,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.2.tgz",
"integrity": "sha512-YF4wgg5iUX5Obywoac27B56FhozMNFhdJSbh11H5GBAwmBtfyRhmgTQqW9tRNNmHDvsZ2CX6UGzoz+YfYn2Leg==",
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.0.tgz",
"integrity": "sha512-JewPyJOYcXBSAS7PIgufscHOwZqhTGyBRiQJhhuDln66tMchxvvt7nJ90JKKIoOelmdHA3kAQbm9cEUHc4JSEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.71.2",
"@typescript-eslint/visitor-keys": "8.71.2"
"@typescript-eslint/types": "8.72.0",
"@typescript-eslint/visitor-keys": "8.72.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1013,9 +1025,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"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==",
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.0.tgz",
"integrity": "sha512-0vbxCf7xLVPSjcOdm6eQbgViH8EDJ8xnl1laLT9yoqSz2YmcSfSg5Vdl0JUvZf6rcuJ0ZivJIuMD4edTg8obaA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1030,15 +1042,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.2.tgz",
"integrity": "sha512-cC8UaalCViJkb3rH/xLIvidEwoFRtMZMUqDjjJMGEKaTFwYRHYBcoczlEGHuvkHEO8svRDrgZ0yQgOArFmwang==",
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.0.tgz",
"integrity": "sha512-gEXxmmbSTSOVTC37IYYvremOM31v+UN2W772bJxmVYPhAOPSUxtacyb7wkm1EhkYKU6KzFJsdU44DCV4to0LWw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.71.2",
"@typescript-eslint/typescript-estree": "8.71.2",
"@typescript-eslint/utils": "8.71.2",
"@typescript-eslint/types": "8.72.0",
"@typescript-eslint/typescript-estree": "8.72.0",
"@typescript-eslint/utils": "8.72.0",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -1055,9 +1067,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"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==",
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.0.tgz",
"integrity": "sha512-Adfoy59kxNEjT5E5fnyrp7GpHYuWs8ONbdohu4ez6IU46qDq8LltG90tLmkucZB2uhYL8mqBjtXqGd8yhfjXqQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1069,16 +1081,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"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==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@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",
"@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",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -1097,16 +1109,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.2.tgz",
"integrity": "sha512-RyKa1G1M9rKHDhrk+HJtiF1MvLD5Ux2JW3TSkzw+02pRALdpJb+QzG1Y5em+LvCgzGwgg8H7mBRFKljNz7ImPw==",
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.0.tgz",
"integrity": "sha512-0up3oArJZBYnzOaEXKo8DmI43QvMZyPoFEt9LWzRJcyl/RcJjfKKPebknN1VbTgXBf3M/KggUh5uUMdURLnseg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.71.2",
"@typescript-eslint/types": "8.71.2",
"@typescript-eslint/typescript-estree": "8.71.2"
"@typescript-eslint/scope-manager": "8.72.0",
"@typescript-eslint/types": "8.72.0",
"@typescript-eslint/typescript-estree": "8.72.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1121,13 +1133,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.71.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.2.tgz",
"integrity": "sha512-zozlN0UA32bSG2zkKAgQyRYSbEISVoyU6vvkKYo2aSny74D8Jh4e+POkJ6Lk0dlezZi5x7TtD+88Xb06IB8nhg==",
"version": "8.72.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.0.tgz",
"integrity": "sha512-E2mroviasaWecDzTGiGMtuEguuLVOeRggnrIn93CJuxYJzQBHbyymlT08aVQydbBal5538o+JkBKXh2sRuCyIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.71.2",
"@typescript-eslint/types": "8.72.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {

View File

@@ -65,18 +65,18 @@
"node": ">=22"
},
"dependencies": {
"@agentclientprotocol/sdk": "2.0.0",
"@anthropic-ai/claude-agent-sdk": "0.3.303",
"@agentclientprotocol/sdk": "2.0.1",
"@anthropic-ai/claude-agent-sdk": "0.3.309",
"diff": "9.0.0",
"zod": "4.6.7"
},
"devDependencies": {
"@anthropic-ai/sdk": "0.135.0",
"@anthropic-ai/sdk": "1.1.0",
"@eslint/js": "10.0.1",
"@tsconfig/node22": "22.0.6",
"@types/node": "26.6.5",
"@typescript-eslint/eslint-plugin": "8.71.2",
"@typescript-eslint/parser": "8.71.2",
"@typescript-eslint/eslint-plugin": "8.72.0",
"@typescript-eslint/parser": "8.72.0",
"ajv": "8.20.0",
"ajv-formats": "3.0.1",
"eslint": "10.13.0",

View File

@@ -5876,9 +5876,28 @@ 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 };
const frame = message as unknown as {
command_uuid: string;
state: string;
wakeup_source?: string;
scheduled_for?: number;
};
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);
@@ -13973,3 +13992,29 @@ 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,18 +458,25 @@ 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. Does nothing while
* such a task is open.
* 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.
*/
async autonomousTurnStarted(): Promise<void> {
if (!this.enabled || this.autonomousTurn) return;
async autonomousTurnStarted(cause?: string): Promise<void> {
if (!this.enabled) return;
if (this.autonomousTurn) {
if (cause) await this.publishProgress(this.autonomousTurn, { description: cause });
return;
}
const task = this.task(`autonomous-turn-${randomUUID()}`);
task.autonomousTurn = true;
task.startedObserved = true;
task.name = "Claude is working";
task.taskType = "turn";
task.description =
this.lastNotificationSummary ?? "Claude Code started a turn without a prompt.";
cause ?? this.lastNotificationSummary ?? "Claude Code started a turn without a prompt.";
task.showInTranscript = false;
this.lastNotificationSummary = undefined;
this.autonomousTurn = task;

View File

@@ -292,9 +292,13 @@ 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 "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
* 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
* 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,6 +2269,57 @@ 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);
@@ -3837,7 +3888,8 @@ 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 "Fork started" result for each of its tool_uses.
// a placeholder result for each of its tool_uses, worded as Claude Code
// 2.1.307+ writes them.
const launching = [
{ type: "text", text: "I'll fork a worker." },
toolUse("toolu_fork", "Agent", {
@@ -3850,7 +3902,15 @@ describe("subagent transcript replay", () => {
const forkStarted = (id: string) => ({
type: "tool_result",
tool_use_id: id,
content: [{ type: "text", text: "Fork started — processing in background" }],
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.",
},
],
});
const trace = await replayRfd(
[
@@ -24163,6 +24223,93 @@ 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

@@ -41,7 +41,8 @@ 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.
* message in a `subagent_type: "fork"` child's transcript, with the tool_result
* text of Claude Code before 2.1.307.
*/
const forkDirective = (uuid: string, launchIds: string[], prompt: string) => ({
...base,

View File

@@ -8,10 +8,18 @@ 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,
@@ -133,12 +141,23 @@ 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)) {