@platless/axom-cli 0.3.13 → 0.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/axom-patch/cli/README.md +30 -8
- package/dist/axom-patch/cli/account-limits/README.md +27 -0
- package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
- package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
- package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
- package/dist/axom-patch/cli/account-limits/generate.py +120 -0
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
- package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
- package/dist/axom-patch/cli/claude-account-limits.json +37 -0
- package/dist/axom-patch/cli/codex-account-limits.json +37 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
- package/dist/axom-patch/cli/nightly-task.md +152 -0
- package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
- package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +5 -1
- package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +1 -1
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +20 -8
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +2 -1
- package/dist/axom-patch/cli/qoder-foreground-output.md +4 -3
- package/dist/axom-patch/cli/qoder-steering.json +15 -15
- package/dist/axom-patch/cli/qoder-subagent-origin.json +21 -21
- package/dist/axom-patch/cli/runtime.json +34 -8
- package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
- package/dist/axom-patch/cli/zcode-models.json +38 -0
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
- package/dist/cli.js +33 -6
- package/dist/keepers/acp/keeper.mjs +3083 -0
- package/dist/keepers/acp/manifest.json +9 -0
- package/dist/keepers/terminal/keeper.mjs +8469 -0
- package/dist/keepers/terminal/manifest.json +9 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
- package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
- package/dist/runtime-deps/qoder-sdk/package.json +8 -0
- package/dist/test/keeperKit.js +67 -0
- package/dist/workstation/acp/acpAgents.js +2163 -611
- package/dist/workstation/acp/acpClient.js +60 -497
- package/dist/workstation/acp/adoption.js +10 -396
- package/dist/workstation/acp/binaryInstall.js +100 -27
- package/dist/workstation/acp/builds/gc.js +245 -0
- package/dist/workstation/acp/builds/install/binary.js +28 -0
- package/dist/workstation/acp/builds/install/npm.js +235 -0
- package/dist/workstation/acp/builds/install/uv.js +254 -0
- package/dist/workstation/acp/builds/migrate.js +195 -0
- package/dist/workstation/acp/builds/model.js +128 -0
- package/dist/workstation/acp/builds/nodeGate.js +100 -0
- package/dist/workstation/acp/builds/store.js +352 -0
- package/dist/workstation/acp/commandTableProbe.js +68 -0
- package/dist/workstation/acp/configOptions.js +103 -1
- package/dist/workstation/acp/envSecrets.js +27 -0
- package/dist/workstation/acp/initializeResponse.js +285 -0
- package/dist/workstation/acp/installErrors.js +12 -2
- package/dist/workstation/acp/installProbe.js +1 -1
- package/dist/workstation/acp/managedPatches.js +76 -160
- package/dist/workstation/acp/outbox.js +330 -0
- package/dist/workstation/acp/patchAssets.js +20 -3
- package/dist/workstation/acp/permission.js +42 -10
- package/dist/workstation/acp/processor/checkpoint.js +87 -0
- package/dist/workstation/acp/processor/commands.js +35 -0
- package/dist/workstation/acp/processor/control.js +355 -0
- package/dist/workstation/acp/processor/emit.js +26 -0
- package/dist/workstation/acp/processor/jsonrpc.js +172 -0
- package/dist/workstation/acp/processor/memoryLog.js +49 -0
- package/dist/workstation/acp/processor/origins.js +48 -0
- package/dist/workstation/acp/processor/output.js +67 -0
- package/dist/workstation/acp/processor/projection.js +42 -0
- package/dist/workstation/acp/processor/record.js +128 -0
- package/dist/workstation/acp/processor/runtime.js +181 -0
- package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
- package/dist/workstation/acp/processor/session.js +525 -0
- package/dist/workstation/acp/processor/sideChannel.js +80 -0
- package/dist/workstation/acp/processor/state.js +74 -0
- package/dist/workstation/acp/processor/step.js +459 -0
- package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
- package/dist/workstation/acp/processor/test/harness.js +120 -0
- package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
- package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
- package/dist/workstation/acp/processor/transport/generations.js +76 -0
- package/dist/workstation/acp/processor/transport/keeper.js +130 -0
- package/dist/workstation/acp/processor/transport/local.js +269 -0
- package/dist/workstation/acp/processor/transport/types.js +78 -0
- package/dist/workstation/acp/processor/updates.js +364 -0
- package/dist/workstation/acp/processorSession.js +1512 -0
- package/dist/workstation/acp/qoderStartup.js +1 -1
- package/dist/workstation/acp/registry/catalog.js +154 -32
- package/dist/workstation/acp/registry/launch.js +61 -119
- package/dist/workstation/acp/sessionUpdateMapper.js +18 -7
- package/dist/workstation/acp/steering.js +58 -0
- package/dist/workstation/acp/switching.js +114 -0
- package/dist/workstation/acp/terminalLogin.js +16 -42
- package/dist/workstation/acp/terminalLoginSpec.js +41 -0
- package/dist/workstation/acp/test/adapterFixtures.js +256 -0
- package/dist/workstation/acp/test/daemonLife.js +106 -0
- package/dist/workstation/acp/test/keeperWorld.js +267 -0
- package/dist/workstation/acp/test/memoryOutbox.js +55 -0
- package/dist/workstation/acp/test/raw/fixtures.js +196 -0
- package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
- package/dist/workstation/acp/test/raw/sanitize.js +344 -0
- package/dist/workstation/acp/test/raw/sources.js +249 -0
- package/dist/workstation/acp/test/realAdapter.js +377 -0
- package/dist/workstation/acp/test/scriptAgent.js +206 -0
- package/dist/workstation/acp/test/sessionFrames.js +42 -0
- package/dist/workstation/acp/timedCommand.js +4 -2
- package/dist/workstation/acp/uvxInstall.js +72 -122
- package/dist/workstation/agentFlow.js +103 -0
- package/dist/workstation/agentSessions.js +1324 -474
- package/dist/workstation/channels/agent.js +7 -5
- package/dist/workstation/channels/computer.js +69 -21
- package/dist/workstation/channels/computerExecs.js +200 -0
- package/dist/workstation/channels/management.js +84 -4
- package/dist/workstation/daemon.js +288 -7
- package/dist/workstation/daemonctl.js +28 -2
- package/dist/workstation/exec/attach.js +126 -0
- package/dist/workstation/exec/command.js +40 -0
- package/dist/workstation/exec/durable.js +517 -0
- package/dist/workstation/exec/fallback.js +106 -0
- package/dist/workstation/exec/policy.js +837 -0
- package/dist/workstation/exec/shaping.js +211 -0
- package/dist/workstation/exec.js +3 -112
- package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
- package/dist/workstation/files/adapters/toolRequests.js +121 -0
- package/dist/workstation/files/client.js +455 -0
- package/dist/workstation/files/errors.js +28 -0
- package/dist/workstation/files/fence.js +350 -0
- package/dist/workstation/files/find/fileIndex.js +153 -0
- package/dist/workstation/files/find/fuzzy.js +155 -0
- package/dist/workstation/files/host.js +122 -0
- package/dist/workstation/files/hostProtocol.js +2 -0
- package/dist/workstation/files/media/cache.js +620 -0
- package/dist/workstation/files/media/encoding.js +308 -0
- package/dist/workstation/files/media/estimator.js +100 -0
- package/dist/workstation/files/media/media.js +325 -0
- package/dist/workstation/files/media/packedAudio.js +23 -0
- package/dist/workstation/files/media/scheduler.js +551 -0
- package/dist/workstation/files/media/types.js +5 -0
- package/dist/workstation/files/scope.js +45 -0
- package/dist/workstation/files/structured.js +284 -0
- package/dist/workstation/{fileOps.js → files/text.js} +117 -38
- package/dist/workstation/files/uploads.js +351 -0
- package/dist/workstation/journal.js +65 -236
- package/dist/workstation/keepers/acp/client.js +101 -0
- package/dist/workstation/keepers/acp/envelope.js +62 -0
- package/dist/workstation/keepers/acp/host.js +419 -0
- package/dist/workstation/keepers/acp/main.js +18 -0
- package/dist/workstation/keepers/acp/memoryStream.js +121 -0
- package/dist/workstation/keepers/acp/params.js +137 -0
- package/dist/workstation/keepers/acp/protocol.js +66 -0
- package/dist/workstation/keepers/acp/recover.js +120 -0
- package/dist/workstation/keepers/acp/session.js +584 -0
- package/dist/workstation/keepers/acp/stderr.js +154 -0
- package/dist/workstation/keepers/kit/activity.js +103 -0
- package/dist/workstation/keepers/kit/bootstrap.js +164 -0
- package/dist/workstation/keepers/kit/buildVersion.js +51 -0
- package/dist/workstation/keepers/kit/bundle.js +120 -0
- package/dist/workstation/keepers/kit/client.js +432 -0
- package/dist/workstation/keepers/kit/identity.js +21 -0
- package/dist/workstation/keepers/kit/ipc.js +112 -0
- package/dist/workstation/keepers/kit/launchConfig.js +8 -0
- package/dist/workstation/keepers/kit/launcher.js +249 -0
- package/dist/workstation/keepers/kit/lock.js +123 -0
- package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
- package/dist/workstation/keepers/kit/paths.js +75 -0
- package/dist/workstation/keepers/kit/protocol.js +43 -0
- package/dist/workstation/keepers/kit/server.js +518 -0
- package/dist/workstation/keepers/kit/streamLog.js +697 -0
- package/dist/workstation/keepers/legacy.js +296 -0
- package/dist/workstation/keepers/policy.js +576 -0
- package/dist/workstation/keepers/stop.js +34 -0
- package/dist/workstation/keepers/terminal/client.js +213 -0
- package/dist/workstation/keepers/terminal/exec.js +981 -0
- package/dist/workstation/keepers/terminal/generations.js +129 -0
- package/dist/workstation/keepers/terminal/keeper.js +62 -0
- package/dist/workstation/keepers/terminal/main.js +43 -0
- package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
- package/dist/workstation/keepers/terminal/params.js +27 -0
- package/dist/workstation/keepers/terminal/protocol.js +56 -0
- package/dist/workstation/keepers/terminal/pty.js +330 -0
- package/dist/workstation/protocol/accountLimits.js +47 -0
- package/dist/workstation/protocol/agent.js +48 -4
- package/dist/workstation/protocol/common.js +30 -0
- package/dist/workstation/protocol/computer.js +15 -4
- package/dist/workstation/protocol/management.js +50 -2
- package/dist/workstation/qoderAdapter/backgroundProcess.js +18 -6
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +78 -147
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
- package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
- package/dist/workstation/qoderAdapter/foregroundOutput.js +15 -9
- package/dist/workstation/qoderAdapter/history.js +1 -3
- package/dist/workstation/qoderAdapter/main.js +4 -2
- package/dist/workstation/qoderAdapter/sdkDriver.js +1 -2
- package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
- package/dist/workstation/qoderAdapter/session.js +1 -1
- package/dist/workstation/qoderAdapter/subwork.js +15 -7
- package/dist/workstation/systemMetrics.js +110 -1
- package/dist/workstation/tools/client.js +9 -1
- package/dist/workstation/tools/info.js +50 -0
- package/dist/workstation/tools/keeperProcess.js +15 -5
- package/dist/workstation/tools/router.js +55 -0
- package/dist/workstation/tools/terminalRouter.js +222 -0
- package/dist/workstation/tools/validation.js +37 -1
- package/dist/workstation/update/coordinator.js +3 -0
- package/dist/workstation/update/ptyPreflight.js +21 -0
- package/dist/workstation/update/taskRegistry.js +105 -11
- package/package.json +10 -7
- package/dist/workstation/acp/acpSession.js +0 -1475
- package/dist/workstation/acp/contract.js +0 -499
- package/dist/workstation/acp/draftInterceptor.js +0 -163
- package/dist/workstation/acp/sessionScope.js +0 -19
- package/dist/workstation/filePaths.js +0 -174
- package/dist/workstation/fileWorker.js +0 -81
- package/dist/workstation/fileWorkerClient.js +0 -121
- package/dist/workstation/qoderAdapter/taskEvent.js +0 -26
- package/dist/workstation/tools/core.js +0 -50
- package/dist/workstation/tools/files.js +0 -525
- package/dist/workstation/tools/terminal.js +0 -214
|
@@ -3,15 +3,13 @@ import { randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { Readable, Writable } from 'node:stream';
|
|
4
4
|
import { client, ndJsonStream, PROTOCOL_VERSION, RequestError } from '@agentclientprotocol/sdk';
|
|
5
5
|
import { sniffAuthUrl } from './authUrlSniffer.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export class UnsupportedNodeVersionError extends Error {
|
|
14
|
-
}
|
|
6
|
+
import { overlayEnvSecrets } from './envSecrets.js';
|
|
7
|
+
import { secretForms } from '../keepers/acp/stderr.js';
|
|
8
|
+
import { initializeRequestParams, jsonRpcErrorCodeOf, parseInitializeResult, } from './initializeResponse.js';
|
|
9
|
+
import { withPathPrefix } from './registry/launch.js';
|
|
10
|
+
// Moved to `initializeResponse.ts` (shared with the deterministic processor); re-exported so every
|
|
11
|
+
// existing importer of these names is unchanged.
|
|
12
|
+
export { STEER_TIMINGS, steeringAdvertised, steeringTimings, toAuthMethodSnapshot, } from './initializeResponse.js';
|
|
15
13
|
/**
|
|
16
14
|
* A subprocess failed to start, or died before it finished responding to `initialize`.
|
|
17
15
|
*
|
|
@@ -19,8 +17,7 @@ export class UnsupportedNodeVersionError extends Error {
|
|
|
19
17
|
* 上写的那几行 —— npx 的 `npm error code ETARGET`、adapter 的崩溃栈、代理的拒绝。这里以前是
|
|
20
18
|
* `stdio: 'inherit'`,那几行只落进 daemon.log,调用方拿到的是一句
|
|
21
19
|
* "exited before responding to initialize (code=1)",服务端那头再转手就成了一条内容为空的
|
|
22
|
-
* "ACP connection closed" ——
|
|
23
|
-
* 需要的是证据,不是从「管子断了」倒推出来的猜测。
|
|
20
|
+
* "ACP connection closed" —— 排查的人手上什么都没有。
|
|
24
21
|
*/
|
|
25
22
|
export class AgentSpawnError extends Error {
|
|
26
23
|
stderr;
|
|
@@ -50,156 +47,15 @@ export class AgentInitializeTimeoutError extends Error {
|
|
|
50
47
|
*/
|
|
51
48
|
const NPM_ETARGET = /(?:^|[^A-Z])ETARGET(?![A-Z])/;
|
|
52
49
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* `InstallOutcome.error`/`stderr` 字符串,不是这两个类里的一个实例,没有 `.stderr` 字段可供
|
|
56
|
-
* {@link isMissingPackageVersion} 去 `instanceof` 窄化。两个调用点都要认同一件事——npm 自己打在
|
|
57
|
-
* stderr 上的 `ETARGET` 错误码——所以正则只在这一处,两个调用点都直接测字符串,而不是各自维护一份。
|
|
50
|
+
* 判定规则本身:npm 自己打在 stderr 上的 `ETARGET` 错误码。读者是安装期的回退
|
|
51
|
+
* (`AcpAgents.installPristineWithFallback`)——启动期不再有回退,启动从不解析版本(spec 2026-10-11 §6.2)。
|
|
58
52
|
*/
|
|
59
53
|
export function stderrShowsMissingPackageVersion(stderr) {
|
|
60
54
|
return NPM_ETARGET.test(stderr);
|
|
61
55
|
}
|
|
62
|
-
/**
|
|
63
|
-
* 这次启动失败,是不是因为 registry 钉的版本 npm 根本没有。
|
|
64
|
-
*
|
|
65
|
-
* 只看 {@link AgentSpawnError.stderr} / {@link AgentInitializeTimeoutError.stderr} 里的原始文本 ——
|
|
66
|
-
* 超时也算进来是因为实测过:npx 在一台慢机器上可以把 20s 的 initialize 预算耗光之后才让子进程死掉,
|
|
67
|
-
* 那时候 ETARGET 早就写在 stderr 上了,只是赢下 race 的是计时器。
|
|
68
|
-
*/
|
|
69
|
-
export function isMissingPackageVersion(error) {
|
|
70
|
-
if (!(error instanceof AgentSpawnError) && !(error instanceof AgentInitializeTimeoutError))
|
|
71
|
-
return false;
|
|
72
|
-
return stderrShowsMissingPackageVersion(error.stderr);
|
|
73
|
-
}
|
|
74
56
|
/** The agent negotiated a protocol version this client does not speak. */
|
|
75
57
|
export class UnsupportedProtocolVersionError extends Error {
|
|
76
58
|
}
|
|
77
|
-
/**
|
|
78
|
-
* Minimum Node.js major version a known agent package requires, keyed by its npx package name
|
|
79
|
-
* (unversioned — matched against both a bare specifier and a version-pinned one; see
|
|
80
|
-
* `argMatchesPackage`).
|
|
81
|
-
*
|
|
82
|
-
* The registry has no field for this — verified fact, not an oversight to fix here. The only place
|
|
83
|
-
* a Node requirement is declared is the agent package's own `package.json#engines`, and `npx` does
|
|
84
|
-
* not enforce it: it just runs the package under whatever `node` is already on PATH. Getting this
|
|
85
|
-
* wrong means the user sees a subprocess crash stack instead of an actionable message (ruling R7),
|
|
86
|
-
* so this table exists to catch the one adapter known, as of 2026-08-21, to need it.
|
|
87
|
-
*/
|
|
88
|
-
const KNOWN_MIN_NODE_MAJOR = [
|
|
89
|
-
{ packageName: '@agentclientprotocol/claude-agent-acp', minMajor: 22 },
|
|
90
|
-
];
|
|
91
|
-
/**
|
|
92
|
-
* True when `arg` is exactly `packageName`, or `packageName` followed by an `@version` suffix.
|
|
93
|
-
* `arg.startsWith(packageName)` alone would also match an unrelated package that merely shares a
|
|
94
|
-
* prefix (e.g. `@agentclientprotocol/claude-agent-acp-extra`); anchoring on an exact match or an
|
|
95
|
-
* `@` boundary rules that out while still matching ruling R3's user-supplied, unpinned
|
|
96
|
-
* `command`/`args` override, which has no version suffix to key off at all.
|
|
97
|
-
*/
|
|
98
|
-
function argMatchesPackage(arg, packageName) {
|
|
99
|
-
return arg === packageName || arg.startsWith(`${packageName}@`);
|
|
100
|
-
}
|
|
101
|
-
/** Exported for tests: derives the minimum Node major version a launch spec requires, if known. */
|
|
102
|
-
export function requiredNodeMajorFor(spec) {
|
|
103
|
-
const match = KNOWN_MIN_NODE_MAJOR.find((entry) => spec.args.some((arg) => argMatchesPackage(arg, entry.packageName)));
|
|
104
|
-
return match?.minMajor;
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Throws a message a user can act on when this machine's Node is older than a known agent
|
|
108
|
-
* requires. `nodeVersion` defaults to the real running Node; it takes an explicit override so tests
|
|
109
|
-
* can exercise both sides of the gate without depending on the host's actual Node version —
|
|
110
|
-
* `process.versions.node` is a frozen property and cannot be stubbed.
|
|
111
|
-
*/
|
|
112
|
-
export function assertNodeVersionSupported(spec, nodeVersion = process.versions.node) {
|
|
113
|
-
const required = requiredNodeMajorFor(spec);
|
|
114
|
-
if (required === undefined)
|
|
115
|
-
return;
|
|
116
|
-
const currentMajor = Number.parseInt(nodeVersion.split('.')[0] ?? '', 10);
|
|
117
|
-
if (Number.isNaN(currentMajor) || currentMajor < required) {
|
|
118
|
-
throw new UnsupportedNodeVersionError(`This agent requires Node.js >= ${required}, but this machine is running Node.js ${nodeVersion}. ` +
|
|
119
|
-
'Install a newer Node.js and try again.');
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Whether an `InitializeResponse._meta` advertises the `_session/steering` extension.
|
|
124
|
-
*
|
|
125
|
-
* ## `_session/steering` is not ACP
|
|
126
|
-
*
|
|
127
|
-
* It is absent from the schema this client is pinned to (`grep steer
|
|
128
|
-
* node_modules/@agentclientprotocol/sdk/dist/schema/types.gen.d.ts` returns nothing). It is an
|
|
129
|
-
* extension `claude-agent-acp` implements: the method constant lives at its `acp-agent.js@0.76.0:164`
|
|
130
|
-
* and its own comment calls it a *"Custom (extension) request method ... Named `_session/steering`
|
|
131
|
-
* per the agreed ACP steering wire protocol"*. The leading underscore is ACP's extension namespace.
|
|
132
|
-
*
|
|
133
|
-
* ## Why the top-level `_meta` specifically
|
|
134
|
-
*
|
|
135
|
-
* The adapter advertises it at `InitializeResponse._meta.steering.supported`, a **sibling of
|
|
136
|
-
* `agentCapabilities`** (`acp-agent.js@0.76.0:944-950`), not a key inside it. `_meta` is typed
|
|
137
|
-
* `{[key: string]: unknown} | null` (`types.gen.d.ts:1498-1500`), so every hop down has to be
|
|
138
|
-
* narrowed rather than indexed through a cast.
|
|
139
|
-
*
|
|
140
|
-
* Strictly `=== true`. An agent that sends `steering: {}` (the shape ACP uses for its *own*
|
|
141
|
-
* capability markers) has not said `supported`, and this extension does not define that spelling —
|
|
142
|
-
* reading it as a yes would be this client inventing a wire contract.
|
|
143
|
-
*/
|
|
144
|
-
export function steeringAdvertised(meta) {
|
|
145
|
-
if (typeof meta !== 'object' || meta === null)
|
|
146
|
-
return false;
|
|
147
|
-
const steering = meta.steering;
|
|
148
|
-
if (typeof steering !== 'object' || steering === null)
|
|
149
|
-
return false;
|
|
150
|
-
return steering.supported === true;
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* The delivery timings the `_session/steering` extension defines — when the agent hands an injected
|
|
154
|
-
* message to the model. `next` is the only lossless one: `now` aborts the running generation and
|
|
155
|
-
* discards whatever tool calls were still outstanding.
|
|
156
|
-
*/
|
|
157
|
-
export const STEER_TIMINGS = ['now', 'next', 'later'];
|
|
158
|
-
/**
|
|
159
|
-
* Which delivery timings an `InitializeResponse._meta` says the agent accepts —
|
|
160
|
-
* `_meta.steering.timings`, the sibling of the `supported` flag {@link steeringAdvertised} reads,
|
|
161
|
-
* and read from the **top-level** `_meta` for exactly the same reason.
|
|
162
|
-
*
|
|
163
|
-
* ## Empty is the answer, not the absence of one
|
|
164
|
-
*
|
|
165
|
-
* An agent that declares no timings has told us we may not name one. That is not "assume the
|
|
166
|
-
* default" — the extension ignores an unrecognised `priority` **silently** (the measurement behind
|
|
167
|
-
* ruling N4: sending `next` to today's adapters lands as `now`, which throws away the user's
|
|
168
|
-
* in-flight work). So an empty result must reach the caller as "do not specify a timing at all",
|
|
169
|
-
* and today — before the upstream PR that makes adapters broadcast this — every agent in the wild
|
|
170
|
-
* produces exactly that. Steady state, not a bug.
|
|
171
|
-
*
|
|
172
|
-
* ## Unknown values are dropped, never rejected
|
|
173
|
-
*
|
|
174
|
-
* A timing this build has never heard of is skipped and the known ones beside it survive. This
|
|
175
|
-
* client is deployed on user machines and can be older than both the agent it launches and the
|
|
176
|
-
* server it reports to; throwing (or discarding the whole array) on a value from a future version
|
|
177
|
-
* of the extension would take steering away from an agent that does support it. Non-strings are
|
|
178
|
-
* dropped the same way: `_meta` is typed `{[key: string]: unknown}`, so nothing here is guaranteed.
|
|
179
|
-
*
|
|
180
|
-
* Duplicates collapse — the wire shape is a JSON array but the fact is a set.
|
|
181
|
-
*/
|
|
182
|
-
export function steeringTimings(meta) {
|
|
183
|
-
if (typeof meta !== 'object' || meta === null)
|
|
184
|
-
return [];
|
|
185
|
-
const steering = meta.steering;
|
|
186
|
-
if (typeof steering !== 'object' || steering === null)
|
|
187
|
-
return [];
|
|
188
|
-
const declared = steering.timings;
|
|
189
|
-
if (!Array.isArray(declared))
|
|
190
|
-
return [];
|
|
191
|
-
const known = [];
|
|
192
|
-
for (const entry of declared) {
|
|
193
|
-
if (typeof entry !== 'string')
|
|
194
|
-
continue;
|
|
195
|
-
if (!STEER_TIMINGS.includes(entry))
|
|
196
|
-
continue;
|
|
197
|
-
if (known.includes(entry))
|
|
198
|
-
continue;
|
|
199
|
-
known.push(entry);
|
|
200
|
-
}
|
|
201
|
-
return known;
|
|
202
|
-
}
|
|
203
59
|
/**
|
|
204
60
|
* The JSON-RPC error code an agent answered with, or `undefined` when the failure was not one an
|
|
205
61
|
* agent answered at all.
|
|
@@ -230,10 +86,7 @@ export function steeringTimings(meta) {
|
|
|
230
86
|
export function jsonRpcErrorCode(error) {
|
|
231
87
|
if (!(error instanceof RequestError))
|
|
232
88
|
return undefined;
|
|
233
|
-
|
|
234
|
-
if (!Number.isInteger(code))
|
|
235
|
-
return undefined;
|
|
236
|
-
return code >= -2_147_483_648 && code <= 2_147_483_647 ? code : undefined;
|
|
89
|
+
return jsonRpcErrorCodeOf(error.code);
|
|
237
90
|
}
|
|
238
91
|
function translateSpawnError(command, error, stderr) {
|
|
239
92
|
if (error.code === 'ENOENT') {
|
|
@@ -274,12 +127,19 @@ const STDERR_FLUSH_GRACE_MS = 250;
|
|
|
274
127
|
*/
|
|
275
128
|
function captureChildStderr(stream, present) {
|
|
276
129
|
if (!stream)
|
|
277
|
-
return { text: () => '', flushed: () => Promise.resolve(),
|
|
130
|
+
return { text: () => '', flushed: () => Promise.resolve(), redactValue: () => { } };
|
|
278
131
|
let tail = '';
|
|
279
132
|
let lineBuffer = '';
|
|
280
133
|
let protectedLine = '';
|
|
281
134
|
let droppingLongLine = false;
|
|
282
|
-
|
|
135
|
+
/** {@link ChildStderr.redactValue}'s forms. */
|
|
136
|
+
const quiet = new Set();
|
|
137
|
+
const redactAll = (text) => {
|
|
138
|
+
let safe = text;
|
|
139
|
+
for (const secret of quiet)
|
|
140
|
+
safe = safe.replaceAll(secret, '[REDACTED]');
|
|
141
|
+
return safe;
|
|
142
|
+
};
|
|
283
143
|
const forward = (value) => {
|
|
284
144
|
tail = (tail + value).slice(-STDERR_TAIL_LIMIT);
|
|
285
145
|
process.stderr.write(value);
|
|
@@ -292,10 +152,7 @@ function captureChildStderr(stream, present) {
|
|
|
292
152
|
droppingLongLine = false;
|
|
293
153
|
}
|
|
294
154
|
else {
|
|
295
|
-
|
|
296
|
-
for (const secret of secrets)
|
|
297
|
-
safe = safe.replaceAll(secret, '[REDACTED]');
|
|
298
|
-
forward(safe);
|
|
155
|
+
forward(redactAll(protectedLine.slice(0, newline + 1)));
|
|
299
156
|
}
|
|
300
157
|
protectedLine = protectedLine.slice(newline + 1);
|
|
301
158
|
}
|
|
@@ -348,8 +205,10 @@ function captureChildStderr(stream, present) {
|
|
|
348
205
|
};
|
|
349
206
|
stream.on('data', (chunk) => {
|
|
350
207
|
const text = chunk.toString();
|
|
351
|
-
if (
|
|
208
|
+
if (quiet.size > 0) {
|
|
352
209
|
forwardProtected(text);
|
|
210
|
+
scanForAuthUrl(text);
|
|
211
|
+
}
|
|
353
212
|
else {
|
|
354
213
|
tail = (tail + text).slice(-STDERR_TAIL_LIMIT);
|
|
355
214
|
process.stderr.write(chunk);
|
|
@@ -362,25 +221,19 @@ function captureChildStderr(stream, present) {
|
|
|
362
221
|
// A final unterminated line (no trailing `\n` before the process exits) is still a real line —
|
|
363
222
|
// scan whatever is left in the buffer once the stream itself says there is no more coming.
|
|
364
223
|
void ended.then(() => {
|
|
365
|
-
if (protectedLine && !droppingLongLine)
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
safe = safe.replaceAll(secret, '[REDACTED]');
|
|
369
|
-
forward(safe);
|
|
370
|
-
}
|
|
371
|
-
if (secrets.size === 0)
|
|
372
|
-
scanForAuthUrl('\n');
|
|
224
|
+
if (protectedLine && !droppingLongLine)
|
|
225
|
+
forward(redactAll(protectedLine));
|
|
226
|
+
scanForAuthUrl('\n');
|
|
373
227
|
});
|
|
374
228
|
return {
|
|
375
229
|
text: () => tail,
|
|
376
230
|
flushed: (ms) => settledWithin(ended, ms).then(() => undefined),
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
secrets.add(secret.slice('Bearer '.length));
|
|
231
|
+
redactValue: (value) => {
|
|
232
|
+
// Longest first, so a form containing another is removed whole (Set order is insertion order).
|
|
233
|
+
const all = [...quiet, ...secretForms(value)].sort((a, b) => b.length - a.length);
|
|
234
|
+
quiet.clear();
|
|
235
|
+
for (const form of all)
|
|
236
|
+
quiet.add(form);
|
|
384
237
|
},
|
|
385
238
|
};
|
|
386
239
|
}
|
|
@@ -404,7 +257,7 @@ function rejectionWithin(failure, ms) {
|
|
|
404
257
|
});
|
|
405
258
|
});
|
|
406
259
|
}
|
|
407
|
-
/** 报错正文里只放最后几行;完整尾巴留在 `.stderr`
|
|
260
|
+
/** 报错正文里只放最后几行;完整尾巴留在 `.stderr` 上给程序读。 */
|
|
408
261
|
function lastStderrLines(text, count) {
|
|
409
262
|
return text.split('\n').map((line) => line.trim()).filter(Boolean).slice(-count).join(' | ');
|
|
410
263
|
}
|
|
@@ -505,54 +358,20 @@ export async function closeAgentTransport(child, connection, graceMs = CHILD_EXI
|
|
|
505
358
|
connection.close();
|
|
506
359
|
await connection.closed.catch(() => { });
|
|
507
360
|
}
|
|
508
|
-
/**
|
|
509
|
-
* One `initialize` auth method, or `null` when the agent did not advertise a usable one.
|
|
510
|
-
*
|
|
511
|
-
* `initResponse` is another process's JSON that arrived over a pipe; `AuthMethod` is what this
|
|
512
|
-
* client *asked* for, not what it was handed. Both variants of that union make `id` and `name`
|
|
513
|
-
* required (`@agentclientprotocol/sdk` `types.gen.d.ts:2241-2246` for `AuthMethodTerminal`,
|
|
514
|
-
* `:2281-2286` for `AuthMethodAgent`), so an agent that omits them is off-spec -- but nothing
|
|
515
|
-
* between its stdout and here checks, and the snapshot then carries `undefined` in two positions
|
|
516
|
-
* its own type declares non-optional. That lie does not stay local: `JSON.stringify` erases an
|
|
517
|
-
* `undefined` value entirely, so the object passes every `typeof x.id` check in this process and
|
|
518
|
-
* still reaches a peer with the keys missing.
|
|
519
|
-
*
|
|
520
|
-
* `agoragentic-acp` advertises exactly `{type: 'terminal', description: '…'}`. Observing it once
|
|
521
|
-
* put a malformed `AuthMethodInfo` in the roster, which rides in every subsequent
|
|
522
|
-
* `management_hello`, which the server then cannot decode -- and a hello the server cannot decode
|
|
523
|
-
* closes the socket with nothing said, so the workstation retried into a permanent OFFLINE.
|
|
524
|
-
*
|
|
525
|
-
* Dropping rather than repairing: `id` is the value `authenticate` is called with, so inventing
|
|
526
|
-
* one would trade a missing sign-in method for a broken one.
|
|
527
|
-
*/
|
|
528
|
-
export function toAuthMethodSnapshot(method) {
|
|
529
|
-
if (typeof method.id !== 'string' || typeof method.name !== 'string')
|
|
530
|
-
return null;
|
|
531
|
-
// Terminal by shape (spec 2026-09-24 D6): a top-level `type: "terminal"`, or a `_meta
|
|
532
|
-
// ["terminal-auth"]` object from an agent that advertises its terminal login only there.
|
|
533
|
-
const terminal = terminalLoginSpecOf(method) !== undefined;
|
|
534
|
-
const args = 'args' in method && Array.isArray(method.args) ? method.args : terminal ? metaTerminalAuthArgs(method) : undefined;
|
|
535
|
-
return {
|
|
536
|
-
id: method.id,
|
|
537
|
-
name: method.name,
|
|
538
|
-
...(terminal ? { type: 'terminal' } : {}),
|
|
539
|
-
...(method.description != null ? { description: method.description } : {}),
|
|
540
|
-
// `args` only exists on a terminal method — top-level, or under `_meta["terminal-auth"]` when
|
|
541
|
-
// that is the only place the agent put it. Never the `_meta` command: see `terminalLogins`.
|
|
542
|
-
...(args ? { args } : {}),
|
|
543
|
-
};
|
|
544
|
-
}
|
|
545
361
|
const DEFAULT_INITIALIZE_TIMEOUT_MS = 30_000;
|
|
546
362
|
/**
|
|
547
363
|
* Spawns an ACP agent subprocess, connects to it over ndjson stdio, and negotiates capabilities
|
|
548
364
|
* via `initialize`. `env` and `agentEnv` are overlay layers on top of the current process
|
|
549
365
|
* environment, not full environments (existing contract carried over from the deleted bridge
|
|
550
366
|
* transport): if `PATH` is present in either, it has already been resolved by the caller and must
|
|
551
|
-
* not be prefixed with anything else here
|
|
367
|
+
* not be prefixed with anything else here — except the spec's own `pathPrefix` (a build's
|
|
368
|
+
* `node_modules/.bin`), which the adapter needs whatever `PATH` it is given.
|
|
552
369
|
*/
|
|
553
370
|
export async function connectAgent(spec, env, agentEnv, options = {}) {
|
|
554
|
-
|
|
555
|
-
|
|
371
|
+
// No Node gate here any more: a build's `engines.node` is checked against the Node that runs it when
|
|
372
|
+
// the spec is made (`launchForBuild`, `builds/nodeGate.ts`); a config `command` override is the user's own.
|
|
373
|
+
// A build's own PATH prefix (`node_modules/.bin`) must survive a server- or agent-supplied PATH.
|
|
374
|
+
const processEnv = withPathPrefix({ ...process.env, ...spec.env, ...env, ...agentEnv }, spec.pathPrefix);
|
|
556
375
|
const child = spawn(spec.command, spec.args, {
|
|
557
376
|
// stderr 是 `pipe` 而不是 `inherit`,而且**依然不接进协议流** —— 它被 `captureChildStderr`
|
|
558
377
|
// 单独读走:原样转发到 daemon.log(`inherit` 以前免费提供的那件事),同时留一份尾巴给启动期的
|
|
@@ -561,131 +380,19 @@ export async function connectAgent(spec, env, agentEnv, options = {}) {
|
|
|
561
380
|
env: processEnv,
|
|
562
381
|
});
|
|
563
382
|
const stderrTail = captureChildStderr(child.stderr, options.present);
|
|
564
|
-
for (const
|
|
565
|
-
stderrTail.
|
|
566
|
-
if (options.onProcessExit)
|
|
567
|
-
child.once('exit', options.onProcessExit);
|
|
383
|
+
for (const value of overlayEnvSecrets({ env, agentEnv }))
|
|
384
|
+
stderrTail.redactValue(value);
|
|
568
385
|
const lifecycle = watchChildLifecycle(child, spec.command, stderrTail);
|
|
569
386
|
// Argument order reads backwards from what the names suggest: argument 1 is the child's stdin
|
|
570
387
|
// (we write to it), argument 2 is the child's stdout (we read from it). Swapping them still
|
|
571
388
|
// compiles and still "connects" — the initialize request just never arrives, so the failure mode
|
|
572
389
|
// is a hang, not an error.
|
|
573
|
-
|
|
574
|
-
// passed in, because the interceptor has to be in place before `initialize` — the agent may send
|
|
575
|
-
// its first update before anyone has had a chance to register anything.
|
|
576
|
-
let updateCapture = null;
|
|
577
|
-
let ownSessionId = null;
|
|
578
|
-
let diagnostics = {
|
|
579
|
-
generation: randomUUID(), revision: 0, available: true,
|
|
580
|
-
launch: { source: options.launchSource ?? 'direct',
|
|
581
|
-
executable: (findOnPath(spec.command, probeEnvironment(processEnv)) ?? spec.command).slice(0, 4096),
|
|
582
|
-
platform: process.platform, architecture: process.arch, transport: 'stdio' },
|
|
583
|
-
};
|
|
584
|
-
const diagnosticListeners = new Set();
|
|
585
|
-
const providerTaskListeners = new Set();
|
|
586
|
-
child.once('exit', () => {
|
|
587
|
-
diagnostics = { ...diagnostics, available: false, revision: diagnostics.revision + 1,
|
|
588
|
-
profile: diagnostics.profile ? { ...diagnostics.profile, observerHealthy: false, rootConfirmed: false } : undefined };
|
|
589
|
-
for (const listener of diagnosticListeners)
|
|
590
|
-
listener(diagnostics);
|
|
591
|
-
});
|
|
592
|
-
let adapterRevision = 0;
|
|
593
|
-
const pendingProfiles = new Map();
|
|
594
|
-
const applyProfile = (sessionId, revision, profile) => {
|
|
595
|
-
const previous = diagnostics.profile;
|
|
596
|
-
if (!previous || !diagnostics.available || sessionId !== ownSessionId || revision <= adapterRevision ||
|
|
597
|
-
!sameProfileIdentity(previous, profile) || profile.rootSessionId !== ownSessionId ||
|
|
598
|
-
(previous.runtimeGeneration !== undefined && profile.runtimeGeneration !== previous.runtimeGeneration) ||
|
|
599
|
-
(profile.rootConfirmed === true && !profile.runtimeGeneration))
|
|
600
|
-
return;
|
|
601
|
-
adapterRevision = revision;
|
|
602
|
-
diagnostics = { ...diagnostics, revision: diagnostics.revision + 1, profile };
|
|
603
|
-
for (const listener of diagnosticListeners)
|
|
604
|
-
listener(diagnostics);
|
|
605
|
-
};
|
|
606
|
-
let draftSink = null;
|
|
607
|
-
// Same "filled in later" shape as `draftSink`, for `bindActivity` — see that method's own doc
|
|
608
|
-
// comment on `AcpConnection`.
|
|
609
|
-
let activitySink = null;
|
|
610
|
-
// Sessions this connection handles by some route other than an `AcpSession` — today only
|
|
611
|
-
// adoption's replay. Kept beside `ownSessionId` rather than folded into it because they mean
|
|
612
|
-
// different things: one is "the session we drive", these are "sessions someone else on this
|
|
613
|
-
// connection is already reading".
|
|
614
|
-
const knownSessionIds = new Set();
|
|
615
|
-
// p3-fix7 N9: updates diverted before any session is bound. An agent may push its first `session/update` (Claude's
|
|
616
|
-
// `available_commands_update`) before this client has processed the `session/new` response that names the session,
|
|
617
|
-
// so it reads as a stranger's. Dropping it lost the command table of every first probe (run 4 and run 5 daemon
|
|
618
|
-
// logs); kept here, bounded, and replayed to the sink once the session binds.
|
|
619
|
-
const preBind = [];
|
|
620
|
-
const stream = ndJsonStream(Writable.toWeb(child.stdin),
|
|
621
|
-
// The child's stdout, minus the notifications the pinned SDK cannot deliver. Everything else
|
|
622
|
-
// reaches it byte-for-byte; see `draftInterceptor.ts` for the measurements behind this.
|
|
623
|
-
interceptDraftUpdates(Readable.toWeb(child.stdout), {
|
|
624
|
-
// Unbound means unknown, and unknown must not be claimed as ours: treating a session we have
|
|
625
|
-
// not been told about as our own would send its frames to the SDK, which drops them silently.
|
|
626
|
-
// Diverting is the recoverable direction — the sink can decide, and it can log.
|
|
627
|
-
isOwnSession: (sessionId) => (ownSessionId !== null && sessionId === ownSessionId) || knownSessionIds.has(sessionId),
|
|
628
|
-
captureUpdate: (notification) => updateCapture?.(notification) ?? false,
|
|
629
|
-
onDiverted: (update) => {
|
|
630
|
-
if (draftSink) {
|
|
631
|
-
draftSink(update);
|
|
632
|
-
return;
|
|
633
|
-
}
|
|
634
|
-
// No sink yet: keep it until the session binds (bounded), then replay what is ours.
|
|
635
|
-
if (preBind.length >= PRE_BIND_MAX_UPDATES) {
|
|
636
|
-
const dropped = preBind.shift();
|
|
637
|
-
warnDroppedPreBind(dropped.update, 'too many updates arrived before the session was bound');
|
|
638
|
-
}
|
|
639
|
-
preBind.push({ update, at: Date.now() });
|
|
640
|
-
},
|
|
641
|
-
}));
|
|
390
|
+
const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
|
|
642
391
|
const app = client({ name: 'axom' });
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
profile.rootSessionId !== params.sessionId || !Number.isSafeInteger(params.revision) ||
|
|
648
|
-
params.revision <= 0 || !sameProfileIdentity(diagnostics.profile, profile))
|
|
649
|
-
return;
|
|
650
|
-
if (ownSessionId) {
|
|
651
|
-
applyProfile(params.sessionId, params.revision, profile);
|
|
652
|
-
}
|
|
653
|
-
else {
|
|
654
|
-
// ACP may notify before its new/load reply. Retain only bounded typed candidates;
|
|
655
|
-
// no candidate becomes evidence until the response binds this connection's exact root.
|
|
656
|
-
const previous = pendingProfiles.get(params.sessionId);
|
|
657
|
-
if (previous && (params.revision <= previous.revision ||
|
|
658
|
-
profile.runtimeGeneration !== previous.profile.runtimeGeneration))
|
|
659
|
-
return;
|
|
660
|
-
if (!previous && pendingProfiles.size >= 8)
|
|
661
|
-
return;
|
|
662
|
-
pendingProfiles.set(params.sessionId, { revision: params.revision, profile });
|
|
663
|
-
}
|
|
664
|
-
});
|
|
665
|
-
app.onNotification('_qoder/task_event', (params) => params, (ctx) => {
|
|
666
|
-
const event = parseTaskObservation(ctx.params);
|
|
667
|
-
if (!event || !taskEventMatchesHost(event, {
|
|
668
|
-
selected: options.providerTaskAdapterSelected === true, available: diagnostics.available,
|
|
669
|
-
confirmed: diagnostics.profile?.rootConfirmed === true, rootSessionId: ownSessionId,
|
|
670
|
-
runtimeGeneration: diagnostics.profile?.runtimeGeneration,
|
|
671
|
-
}))
|
|
672
|
-
return;
|
|
673
|
-
for (const listener of providerTaskListeners)
|
|
674
|
-
listener(event);
|
|
675
|
-
});
|
|
676
|
-
// claude-agent-acp's raw-SDK-message channel (design §5.1/§2.1): registered unconditionally on
|
|
677
|
-
// every connection, before `options.configure?.(app)` and before `initialize` — `_claude/sdkMessage`
|
|
678
|
-
// is not a built-in ACP method, so `ClientApp.onNotification` needs the custom-method overload (a
|
|
679
|
-
// parser plus a handler) rather than the typed-literal one `registerPermissionHandlers` uses. The
|
|
680
|
-
// method name is vendor-specific, so this handler simply never fires on a connection to any other
|
|
681
|
-
// agent — there is no need to gate the registration on `agentInfo`, which in any case is not yet
|
|
682
|
-
// known this early (`initialize` has not happened).
|
|
683
|
-
app.onNotification('_claude/sdkMessage', (params) => (params ?? {}), (ctx) => {
|
|
684
|
-
const raw = claudeActivityRaw(ctx.params);
|
|
685
|
-
if (raw)
|
|
686
|
-
activitySink?.(raw);
|
|
687
|
-
});
|
|
688
|
-
options.configure?.(app);
|
|
392
|
+
// This adapter process's generation: the scope `permission.ts`'s `permissionRequestId` derives card ids in (a sign-in's
|
|
393
|
+
// connection registers the permission handlers).
|
|
394
|
+
const generation = randomUUID();
|
|
395
|
+
options.configure?.(app, { generation });
|
|
689
396
|
const connection = app.connect(stream);
|
|
690
397
|
const timeoutMs = options.initializeTimeoutMs ?? DEFAULT_INITIALIZE_TIMEOUT_MS;
|
|
691
398
|
let timeoutHandle;
|
|
@@ -702,60 +409,7 @@ export async function connectAgent(spec, env, agentEnv, options = {}) {
|
|
|
702
409
|
let initResponse;
|
|
703
410
|
try {
|
|
704
411
|
initResponse = await Promise.race([
|
|
705
|
-
connection.agent.request('initialize',
|
|
706
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
707
|
-
// Three things are declared here, and they are not interchangeable — see
|
|
708
|
-
// `draftSubagents.ts` for the tier each one sits in.
|
|
709
|
-
//
|
|
710
|
-
// 1. `subagents: {}` is the **canonical** draft field from ACP RFD #1992. Both official
|
|
711
|
-
// adapters gate native subagent sessions on it, and both answer with
|
|
712
|
-
// `agentCapabilities.sessionCapabilities.subagents` (measured against each).
|
|
713
|
-
//
|
|
714
|
-
// 2. The AIR list is sent as well, for two separate reasons. It is the *only* channel for
|
|
715
|
-
// `asyncTasks` — nothing canonical exists for background work — and it is the fallback
|
|
716
|
-
// for `nativeSubagentSessions`, because a released SDK sitting between us and an agent
|
|
717
|
-
// can strip a draft field it has no type for (upstream says so in #1017's compatibility
|
|
718
|
-
// note). Declaring both is not belt-and-braces: they cover different failure modes.
|
|
719
|
-
//
|
|
720
|
-
// 3. `subagent-transcript: true` stays. It is claude-agent-acp's older vendor key
|
|
721
|
-
// (`dist/acp-agent.js:155-158`), and it still governs the **fallback** path taken when
|
|
722
|
-
// native subagent sessions are not negotiated — an older adapter, or a stripped
|
|
723
|
-
// capability. Without it that path strips every subagent's text and thinking
|
|
724
|
-
// (`stripSubagentTextAndThinking`, `:3926`), leaving nothing but tool-call wreckage.
|
|
725
|
-
// Adapters that have never heard of any of these keys are required by the spec to
|
|
726
|
-
// ignore them.
|
|
727
|
-
clientCapabilities: asSdkClientCapabilities({
|
|
728
|
-
[DRAFT_SUBAGENTS_CAPABILITY]: {},
|
|
729
|
-
// `ClientSessionCapabilities.configOptions` (`types.gen.d.ts:4306`), part of the schema
|
|
730
|
-
// proper — unlike `subagents` above, this survives the pinned SDK's `zInitializeRequest`
|
|
731
|
-
// stripping because it's a known field, not a draft one. `{}` for `boolean` tells the
|
|
732
|
-
// agent it may send `type: "boolean"` entries in `SessionConfigOption` and that this
|
|
733
|
-
// client will send `session/set_config_option` with a boolean `value`
|
|
734
|
-
// (`SessionConfigOptionsCapabilities`, `types.gen.d.ts:4333-4353`); without it an agent
|
|
735
|
-
// with a boolean toggle downgrades it to a two-value select.
|
|
736
|
-
session: { configOptions: { boolean: {} } },
|
|
737
|
-
// `ClientCapabilities.elicitation` (`types.gen.d.ts:4479`) is a known field of the schema
|
|
738
|
-
// proper, not a draft one, so it survives `zInitializeRequest` stripping as-is. `{}` for
|
|
739
|
-
// `url` (`ElicitationUrlCapabilities`, `:4616`) means the client can put a URL in front of
|
|
740
|
-
// the user; this is the precondition for the whole device-code login shape to exist at all
|
|
741
|
-
// — Codex's own `getCodexAuthMethods` gates its `chat-gpt-device-code` auth method on
|
|
742
|
-
// `clientCapabilities?.elicitation?.url != null`. Without declaring it, Codex never offers
|
|
743
|
-
// that method and `elicitation/create` with `mode: 'url'` never arrives to answer (see
|
|
744
|
-
// `permission.ts`'s `handleCreateElicitation`).
|
|
745
|
-
//
|
|
746
|
-
// `form` (`ElicitationFormCapabilities`, `types.gen.d.ts:4455`) is what keeps Claude's
|
|
747
|
-
// AskUserQuestion alive: without it the adapter puts the tool in `disallowedTools`
|
|
748
|
-
// (`claude-agent-acp/dist/acp-agent.js:5959`), so the model never has it at all — the
|
|
749
|
-
// symptom is prose questions, not an unrendered card. This is the trigger for the whole
|
|
750
|
-
// elicitation-standardization plan: everything through Task 17 built the machinery to
|
|
751
|
-
// answer `elicitation/create` once it arrives; this line is what makes it arrive at all.
|
|
752
|
-
elicitation: { url: {}, form: {} },
|
|
753
|
-
_meta: {
|
|
754
|
-
'subagent-transcript': true,
|
|
755
|
-
...airCapabilityMeta(AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY, AIR_ASYNC_TASKS_CAPABILITY),
|
|
756
|
-
},
|
|
757
|
-
}),
|
|
758
|
-
}),
|
|
412
|
+
connection.agent.request('initialize', initializeRequestParams()),
|
|
759
413
|
lifecycle.failure,
|
|
760
414
|
timeout,
|
|
761
415
|
]);
|
|
@@ -776,130 +430,39 @@ export async function connectAgent(spec, env, agentEnv, options = {}) {
|
|
|
776
430
|
}
|
|
777
431
|
// Global Constraint 1 is v1-only; the schema calls for disconnecting when the agent didn't
|
|
778
432
|
// accept the version this client sent.
|
|
779
|
-
|
|
433
|
+
const reading = parseInitializeResult(initResponse);
|
|
434
|
+
if (!reading.protocolSupported) {
|
|
780
435
|
const error = new UnsupportedProtocolVersionError(`Agent '${spec.command}' negotiated protocol version ${initResponse.protocolVersion}, ` +
|
|
781
436
|
`but this client only supports version ${PROTOCOL_VERSION}.`);
|
|
782
437
|
connection.close(error);
|
|
783
438
|
child.kill();
|
|
784
439
|
throw error;
|
|
785
440
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
const capabilities =
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
// correctly by accident but reads `null`/`undefined` correctly too, which hides the real bug:
|
|
792
|
-
// this must stay an explicit `!= null`, never `Boolean(...)` or a bare truthy test.
|
|
793
|
-
list: sessionCapabilities?.list != null,
|
|
794
|
-
delete: sessionCapabilities?.delete != null,
|
|
795
|
-
resume: sessionCapabilities?.resume != null,
|
|
796
|
-
loadSession: initResponse.agentCapabilities?.loadSession === true,
|
|
797
|
-
logout: initResponse.agentCapabilities?.auth?.logout != null,
|
|
798
|
-
/**
|
|
799
|
-
* ACP `PromptCapabilities.image` (`types.gen.d.ts:1601`): "Agent supports
|
|
800
|
-
* [`ContentBlock::Image`]". Every field of that type is optional, so an agent that says
|
|
801
|
-
* nothing supports text only -- `=== true` rather than `!== false`.
|
|
802
|
-
*
|
|
803
|
-
* Verified against codex-acp 1.7.0, which declares `{embeddedContext: true, image: true}`.
|
|
804
|
-
* `audio` is deliberately not read: no agent we target declares it, and an unread capability
|
|
805
|
-
* is a field nobody can act on.
|
|
806
|
-
*/
|
|
807
|
-
promptImage: initResponse.agentCapabilities?.promptCapabilities?.image === true,
|
|
808
|
-
mcpHttp: initResponse.agentCapabilities?.mcpCapabilities?.http === true,
|
|
809
|
-
};
|
|
810
|
-
// Honour the advertised capability, not what the agent happens to route: some adapters (e.g.
|
|
811
|
-
// codex-acp routing `session/fork`) implement more than they declare in `agentCapabilities`.
|
|
812
|
-
// Reading `capabilities` above off `initResponse` — never probing the agent for what it accepts
|
|
813
|
-
// — is what keeps that distinction intact.
|
|
814
|
-
const advertisedAuthMethods = initResponse.authMethods ?? [];
|
|
815
|
-
const authMethods = advertisedAuthMethods
|
|
816
|
-
.map(toAuthMethodSnapshot)
|
|
817
|
-
.filter((method) => method !== null);
|
|
818
|
-
const terminalLogins = new Map();
|
|
819
|
-
for (const method of advertisedAuthMethods) {
|
|
820
|
-
const spec = terminalLoginSpecOf(method);
|
|
821
|
-
if (spec && typeof method.id === 'string')
|
|
822
|
-
terminalLogins.set(method.id, spec);
|
|
823
|
-
}
|
|
824
|
-
if (authMethods.length !== advertisedAuthMethods.length) {
|
|
441
|
+
// Capabilities, auth methods and steering are read in `initializeResponse.ts`, the one reading the
|
|
442
|
+
// deterministic processor shares — see that file for the rules.
|
|
443
|
+
const { capabilities, authMethods, droppedAuthMethods } = reading;
|
|
444
|
+
const terminalLogins = new Map(reading.terminalLogins);
|
|
445
|
+
if (droppedAuthMethods > 0) {
|
|
825
446
|
// Named and counted rather than dropped in silence: the user loses a sign-in method they can
|
|
826
447
|
// see advertised in the agent's own docs, and the only honest answer to "why" is which agent
|
|
827
448
|
// sent what. `agentInfo.name` first because most agents launch through `npx`/`uvx` and
|
|
828
449
|
// `spec.command` would then name the launcher, which identifies none of the ~40 registry
|
|
829
450
|
// entries. See `toAuthMethodSnapshot` for what accepting these costs instead.
|
|
830
451
|
console.warn(`[acp] agent '${initResponse.agentInfo?.name ?? spec.command}' advertised ` +
|
|
831
|
-
`${
|
|
452
|
+
`${droppedAuthMethods} auth method(s) without the required ` +
|
|
832
453
|
'id/name; ignoring them');
|
|
833
454
|
}
|
|
834
455
|
return {
|
|
835
|
-
get runtimeDiagnostics() { return diagnostics; },
|
|
836
|
-
providerTaskAdapterSelected: options.providerTaskAdapterSelected === true,
|
|
837
|
-
publishCallbacks(snapshot) {
|
|
838
|
-
if (snapshot.rootSessionId !== ownSessionId)
|
|
839
|
-
return;
|
|
840
|
-
diagnostics = { ...diagnostics, revision: diagnostics.revision + 1, callbacks: snapshot };
|
|
841
|
-
for (const listener of diagnosticListeners)
|
|
842
|
-
listener(diagnostics);
|
|
843
|
-
},
|
|
844
|
-
onRuntimeDiagnostics(cb) { diagnosticListeners.add(cb); },
|
|
845
|
-
onProviderTaskEvent(cb) { providerTaskListeners.add(cb); },
|
|
846
456
|
capabilities,
|
|
847
457
|
authMethods,
|
|
848
458
|
terminalLogins,
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
459
|
+
supportsAccountLimits: reading.supportsAccountLimits,
|
|
460
|
+
supportsSteering: reading.supportsSteering,
|
|
461
|
+
steeringTimings: reading.steeringTimings,
|
|
462
|
+
agentInfo: reading.agentInfo,
|
|
852
463
|
connection,
|
|
853
|
-
redactStderrSecret: (secret) => stderrTail.redactSecret(secret),
|
|
854
|
-
agentProcessAlive: () => child.exitCode === null && child.signalCode === null,
|
|
855
|
-
captureSessionUpdates(sink) {
|
|
856
|
-
if (updateCapture)
|
|
857
|
-
throw new Error('A session replay is already active on this connection');
|
|
858
|
-
updateCapture = sink;
|
|
859
|
-
return () => { if (updateCapture === sink)
|
|
860
|
-
updateCapture = null; };
|
|
861
|
-
},
|
|
862
|
-
bindDraftUpdates(sessionId, sink) {
|
|
863
|
-
ownSessionId = sessionId;
|
|
864
|
-
draftSink = sink;
|
|
865
|
-
const current = diagnostics.profile;
|
|
866
|
-
if (current?.rootSessionId && current.rootSessionId !== sessionId) {
|
|
867
|
-
diagnostics = { ...diagnostics, profile: { ...current, rootSessionId: undefined,
|
|
868
|
-
runtimeGeneration: undefined, rootConfirmed: false, initialized: false, observerHealthy: false } };
|
|
869
|
-
}
|
|
870
|
-
const pending = pendingProfiles.get(sessionId);
|
|
871
|
-
pendingProfiles.clear();
|
|
872
|
-
if (pending)
|
|
873
|
-
applyProfile(sessionId, pending.revision, pending.profile);
|
|
874
|
-
// p3-fix7 N9: replay what arrived before this moment. Our own session's updates go to the sink as `pre-bind`
|
|
875
|
-
// (they are ours, not a child's); anything else, or anything older than the window, is dropped with the
|
|
876
|
-
// warning it always had.
|
|
877
|
-
const now = Date.now();
|
|
878
|
-
for (const { update, at } of preBind.splice(0)) {
|
|
879
|
-
if (update.sessionId === sessionId && now - at <= PRE_BIND_MAX_AGE_MS) {
|
|
880
|
-
sink(update.reason === 'foreign-session' ? { ...update, reason: 'pre-bind' } : update);
|
|
881
|
-
}
|
|
882
|
-
else {
|
|
883
|
-
warnDroppedPreBind(update, 'no session is bound to this connection yet');
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
},
|
|
887
|
-
bindActivity(sink) {
|
|
888
|
-
activitySink = sink;
|
|
889
|
-
},
|
|
890
|
-
markSessionKnown(sessionId) {
|
|
891
|
-
knownSessionIds.add(sessionId);
|
|
892
|
-
return () => knownSessionIds.delete(sessionId);
|
|
893
|
-
},
|
|
894
464
|
// Child first, streams second — see `closeAgentTransport` for why that order is the fix and not
|
|
895
465
|
// an incidental detail.
|
|
896
466
|
close: () => closeAgentTransport(child, connection),
|
|
897
467
|
};
|
|
898
468
|
}
|
|
899
|
-
/** p3-fix7 N9: how many updates, and for how long, a connection keeps before its session is bound. */
|
|
900
|
-
const PRE_BIND_MAX_UPDATES = 32;
|
|
901
|
-
const PRE_BIND_MAX_AGE_MS = 5_000;
|
|
902
|
-
/** Says so once per update rather than dropping in silence; it should stay rare, and if it stops being rare it is worth seeing. */
|
|
903
|
-
function warnDroppedPreBind(update, why) {
|
|
904
|
-
console.warn(`[acp] dropped a ${update.reason} session/update for '${update.sessionId}': ${why}`);
|
|
905
|
-
}
|