@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.
Files changed (235) hide show
  1. package/dist/axom-patch/cli/README.md +30 -8
  2. package/dist/axom-patch/cli/account-limits/README.md +27 -0
  3. package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
  4. package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
  5. package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
  6. package/dist/axom-patch/cli/account-limits/generate.py +120 -0
  7. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
  8. package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
  9. package/dist/axom-patch/cli/claude-account-limits.json +37 -0
  10. package/dist/axom-patch/cli/codex-account-limits.json +37 -0
  11. package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
  12. package/dist/axom-patch/cli/nightly-task.md +152 -0
  13. package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
  14. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +5 -1
  15. package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
  16. package/dist/axom-patch/cli/probes/qoder-steering.mjs +1 -1
  17. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +20 -8
  18. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
  19. package/dist/axom-patch/cli/qoder-background-terminal.md +2 -1
  20. package/dist/axom-patch/cli/qoder-foreground-output.md +4 -3
  21. package/dist/axom-patch/cli/qoder-steering.json +15 -15
  22. package/dist/axom-patch/cli/qoder-subagent-origin.json +21 -21
  23. package/dist/axom-patch/cli/runtime.json +34 -8
  24. package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
  25. package/dist/axom-patch/cli/zcode-models.json +38 -0
  26. package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
  27. package/dist/cli.js +33 -6
  28. package/dist/keepers/acp/keeper.mjs +3083 -0
  29. package/dist/keepers/acp/manifest.json +9 -0
  30. package/dist/keepers/terminal/keeper.mjs +8469 -0
  31. package/dist/keepers/terminal/manifest.json +9 -0
  32. package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  33. package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  34. package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
  35. package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
  36. package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
  37. package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
  38. package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
  39. package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
  40. package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
  41. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
  42. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
  43. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
  44. package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  45. package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
  46. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  47. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  48. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  49. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  50. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  51. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  52. package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
  53. package/dist/runtime-deps/qoder-sdk/package.json +8 -0
  54. package/dist/test/keeperKit.js +67 -0
  55. package/dist/workstation/acp/acpAgents.js +2163 -611
  56. package/dist/workstation/acp/acpClient.js +60 -497
  57. package/dist/workstation/acp/adoption.js +10 -396
  58. package/dist/workstation/acp/binaryInstall.js +100 -27
  59. package/dist/workstation/acp/builds/gc.js +245 -0
  60. package/dist/workstation/acp/builds/install/binary.js +28 -0
  61. package/dist/workstation/acp/builds/install/npm.js +235 -0
  62. package/dist/workstation/acp/builds/install/uv.js +254 -0
  63. package/dist/workstation/acp/builds/migrate.js +195 -0
  64. package/dist/workstation/acp/builds/model.js +128 -0
  65. package/dist/workstation/acp/builds/nodeGate.js +100 -0
  66. package/dist/workstation/acp/builds/store.js +352 -0
  67. package/dist/workstation/acp/commandTableProbe.js +68 -0
  68. package/dist/workstation/acp/configOptions.js +103 -1
  69. package/dist/workstation/acp/envSecrets.js +27 -0
  70. package/dist/workstation/acp/initializeResponse.js +285 -0
  71. package/dist/workstation/acp/installErrors.js +12 -2
  72. package/dist/workstation/acp/installProbe.js +1 -1
  73. package/dist/workstation/acp/managedPatches.js +76 -160
  74. package/dist/workstation/acp/outbox.js +330 -0
  75. package/dist/workstation/acp/patchAssets.js +20 -3
  76. package/dist/workstation/acp/permission.js +42 -10
  77. package/dist/workstation/acp/processor/checkpoint.js +87 -0
  78. package/dist/workstation/acp/processor/commands.js +35 -0
  79. package/dist/workstation/acp/processor/control.js +355 -0
  80. package/dist/workstation/acp/processor/emit.js +26 -0
  81. package/dist/workstation/acp/processor/jsonrpc.js +172 -0
  82. package/dist/workstation/acp/processor/memoryLog.js +49 -0
  83. package/dist/workstation/acp/processor/origins.js +48 -0
  84. package/dist/workstation/acp/processor/output.js +67 -0
  85. package/dist/workstation/acp/processor/projection.js +42 -0
  86. package/dist/workstation/acp/processor/record.js +128 -0
  87. package/dist/workstation/acp/processor/runtime.js +181 -0
  88. package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
  89. package/dist/workstation/acp/processor/session.js +525 -0
  90. package/dist/workstation/acp/processor/sideChannel.js +80 -0
  91. package/dist/workstation/acp/processor/state.js +74 -0
  92. package/dist/workstation/acp/processor/step.js +459 -0
  93. package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
  94. package/dist/workstation/acp/processor/test/harness.js +120 -0
  95. package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
  96. package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
  97. package/dist/workstation/acp/processor/transport/generations.js +76 -0
  98. package/dist/workstation/acp/processor/transport/keeper.js +130 -0
  99. package/dist/workstation/acp/processor/transport/local.js +269 -0
  100. package/dist/workstation/acp/processor/transport/types.js +78 -0
  101. package/dist/workstation/acp/processor/updates.js +364 -0
  102. package/dist/workstation/acp/processorSession.js +1512 -0
  103. package/dist/workstation/acp/qoderStartup.js +1 -1
  104. package/dist/workstation/acp/registry/catalog.js +154 -32
  105. package/dist/workstation/acp/registry/launch.js +61 -119
  106. package/dist/workstation/acp/sessionUpdateMapper.js +18 -7
  107. package/dist/workstation/acp/steering.js +58 -0
  108. package/dist/workstation/acp/switching.js +114 -0
  109. package/dist/workstation/acp/terminalLogin.js +16 -42
  110. package/dist/workstation/acp/terminalLoginSpec.js +41 -0
  111. package/dist/workstation/acp/test/adapterFixtures.js +256 -0
  112. package/dist/workstation/acp/test/daemonLife.js +106 -0
  113. package/dist/workstation/acp/test/keeperWorld.js +267 -0
  114. package/dist/workstation/acp/test/memoryOutbox.js +55 -0
  115. package/dist/workstation/acp/test/raw/fixtures.js +196 -0
  116. package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
  117. package/dist/workstation/acp/test/raw/sanitize.js +344 -0
  118. package/dist/workstation/acp/test/raw/sources.js +249 -0
  119. package/dist/workstation/acp/test/realAdapter.js +377 -0
  120. package/dist/workstation/acp/test/scriptAgent.js +206 -0
  121. package/dist/workstation/acp/test/sessionFrames.js +42 -0
  122. package/dist/workstation/acp/timedCommand.js +4 -2
  123. package/dist/workstation/acp/uvxInstall.js +72 -122
  124. package/dist/workstation/agentFlow.js +103 -0
  125. package/dist/workstation/agentSessions.js +1324 -474
  126. package/dist/workstation/channels/agent.js +7 -5
  127. package/dist/workstation/channels/computer.js +69 -21
  128. package/dist/workstation/channels/computerExecs.js +200 -0
  129. package/dist/workstation/channels/management.js +84 -4
  130. package/dist/workstation/daemon.js +288 -7
  131. package/dist/workstation/daemonctl.js +28 -2
  132. package/dist/workstation/exec/attach.js +126 -0
  133. package/dist/workstation/exec/command.js +40 -0
  134. package/dist/workstation/exec/durable.js +517 -0
  135. package/dist/workstation/exec/fallback.js +106 -0
  136. package/dist/workstation/exec/policy.js +837 -0
  137. package/dist/workstation/exec/shaping.js +211 -0
  138. package/dist/workstation/exec.js +3 -112
  139. package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
  140. package/dist/workstation/files/adapters/toolRequests.js +121 -0
  141. package/dist/workstation/files/client.js +455 -0
  142. package/dist/workstation/files/errors.js +28 -0
  143. package/dist/workstation/files/fence.js +350 -0
  144. package/dist/workstation/files/find/fileIndex.js +153 -0
  145. package/dist/workstation/files/find/fuzzy.js +155 -0
  146. package/dist/workstation/files/host.js +122 -0
  147. package/dist/workstation/files/hostProtocol.js +2 -0
  148. package/dist/workstation/files/media/cache.js +620 -0
  149. package/dist/workstation/files/media/encoding.js +308 -0
  150. package/dist/workstation/files/media/estimator.js +100 -0
  151. package/dist/workstation/files/media/media.js +325 -0
  152. package/dist/workstation/files/media/packedAudio.js +23 -0
  153. package/dist/workstation/files/media/scheduler.js +551 -0
  154. package/dist/workstation/files/media/types.js +5 -0
  155. package/dist/workstation/files/scope.js +45 -0
  156. package/dist/workstation/files/structured.js +284 -0
  157. package/dist/workstation/{fileOps.js → files/text.js} +117 -38
  158. package/dist/workstation/files/uploads.js +351 -0
  159. package/dist/workstation/journal.js +65 -236
  160. package/dist/workstation/keepers/acp/client.js +101 -0
  161. package/dist/workstation/keepers/acp/envelope.js +62 -0
  162. package/dist/workstation/keepers/acp/host.js +419 -0
  163. package/dist/workstation/keepers/acp/main.js +18 -0
  164. package/dist/workstation/keepers/acp/memoryStream.js +121 -0
  165. package/dist/workstation/keepers/acp/params.js +137 -0
  166. package/dist/workstation/keepers/acp/protocol.js +66 -0
  167. package/dist/workstation/keepers/acp/recover.js +120 -0
  168. package/dist/workstation/keepers/acp/session.js +584 -0
  169. package/dist/workstation/keepers/acp/stderr.js +154 -0
  170. package/dist/workstation/keepers/kit/activity.js +103 -0
  171. package/dist/workstation/keepers/kit/bootstrap.js +164 -0
  172. package/dist/workstation/keepers/kit/buildVersion.js +51 -0
  173. package/dist/workstation/keepers/kit/bundle.js +120 -0
  174. package/dist/workstation/keepers/kit/client.js +432 -0
  175. package/dist/workstation/keepers/kit/identity.js +21 -0
  176. package/dist/workstation/keepers/kit/ipc.js +112 -0
  177. package/dist/workstation/keepers/kit/launchConfig.js +8 -0
  178. package/dist/workstation/keepers/kit/launcher.js +249 -0
  179. package/dist/workstation/keepers/kit/lock.js +123 -0
  180. package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
  181. package/dist/workstation/keepers/kit/paths.js +75 -0
  182. package/dist/workstation/keepers/kit/protocol.js +43 -0
  183. package/dist/workstation/keepers/kit/server.js +518 -0
  184. package/dist/workstation/keepers/kit/streamLog.js +697 -0
  185. package/dist/workstation/keepers/legacy.js +296 -0
  186. package/dist/workstation/keepers/policy.js +576 -0
  187. package/dist/workstation/keepers/stop.js +34 -0
  188. package/dist/workstation/keepers/terminal/client.js +213 -0
  189. package/dist/workstation/keepers/terminal/exec.js +981 -0
  190. package/dist/workstation/keepers/terminal/generations.js +129 -0
  191. package/dist/workstation/keepers/terminal/keeper.js +62 -0
  192. package/dist/workstation/keepers/terminal/main.js +43 -0
  193. package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
  194. package/dist/workstation/keepers/terminal/params.js +27 -0
  195. package/dist/workstation/keepers/terminal/protocol.js +56 -0
  196. package/dist/workstation/keepers/terminal/pty.js +330 -0
  197. package/dist/workstation/protocol/accountLimits.js +47 -0
  198. package/dist/workstation/protocol/agent.js +48 -4
  199. package/dist/workstation/protocol/common.js +30 -0
  200. package/dist/workstation/protocol/computer.js +15 -4
  201. package/dist/workstation/protocol/management.js +50 -2
  202. package/dist/workstation/qoderAdapter/backgroundProcess.js +18 -6
  203. package/dist/workstation/qoderAdapter/backgroundTerminal.js +78 -147
  204. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
  205. package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
  206. package/dist/workstation/qoderAdapter/foregroundOutput.js +15 -9
  207. package/dist/workstation/qoderAdapter/history.js +1 -3
  208. package/dist/workstation/qoderAdapter/main.js +4 -2
  209. package/dist/workstation/qoderAdapter/sdkDriver.js +1 -2
  210. package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
  211. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
  212. package/dist/workstation/qoderAdapter/session.js +1 -1
  213. package/dist/workstation/qoderAdapter/subwork.js +15 -7
  214. package/dist/workstation/systemMetrics.js +110 -1
  215. package/dist/workstation/tools/client.js +9 -1
  216. package/dist/workstation/tools/info.js +50 -0
  217. package/dist/workstation/tools/keeperProcess.js +15 -5
  218. package/dist/workstation/tools/router.js +55 -0
  219. package/dist/workstation/tools/terminalRouter.js +222 -0
  220. package/dist/workstation/tools/validation.js +37 -1
  221. package/dist/workstation/update/coordinator.js +3 -0
  222. package/dist/workstation/update/ptyPreflight.js +21 -0
  223. package/dist/workstation/update/taskRegistry.js +105 -11
  224. package/package.json +10 -7
  225. package/dist/workstation/acp/acpSession.js +0 -1475
  226. package/dist/workstation/acp/contract.js +0 -499
  227. package/dist/workstation/acp/draftInterceptor.js +0 -163
  228. package/dist/workstation/acp/sessionScope.js +0 -19
  229. package/dist/workstation/filePaths.js +0 -174
  230. package/dist/workstation/fileWorker.js +0 -81
  231. package/dist/workstation/fileWorkerClient.js +0 -121
  232. package/dist/workstation/qoderAdapter/taskEvent.js +0 -26
  233. package/dist/workstation/tools/core.js +0 -50
  234. package/dist/workstation/tools/files.js +0 -525
  235. 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 { metaTerminalAuthArgs, terminalLoginSpecOf } from './terminalLogin.js';
7
- import { airCapabilityMeta, asSdkClientCapabilities, AIR_ASYNC_TASKS_CAPABILITY, AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY, DRAFT_SUBAGENTS_CAPABILITY, } from './draftSubagents.js';
8
- import { interceptDraftUpdates } from './draftInterceptor.js';
9
- import { claudeActivityRaw } from './activityReporting.js';
10
- import { findOnPath, probeEnvironment } from './installProbe.js';
11
- import { parseQoderProfile, sameProfileIdentity } from '../qoderAdapter/profile.js';
12
- import { parseTaskObservation, taskEventMatchesHost } from '../qoderAdapter/taskEvent.js';
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" —— 排查的人手上什么都没有。见 {@link isMissingPackageVersion}:那条判定
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
- * 判定规则本身,脱离 {@link AgentSpawnError}/{@link AgentInitializeTimeoutError} 单独导出 ——
54
- * `installNpxPackage`(`installProbe.ts`)是这条规则的第二个读者:它下载适配器失败时报的是一段
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
- const { code } = error;
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(), redactSecret: () => { } };
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
- const secrets = new Set();
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
- let safe = protectedLine.slice(0, newline + 1);
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 (secrets.size > 0)
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
- let safe = protectedLine;
367
- for (const secret of secrets)
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
- redactSecret: (secret) => {
378
- if (!secret)
379
- return;
380
- lineBuffer = '';
381
- secrets.add(secret);
382
- if (secret.startsWith('Bearer '))
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` 上给 {@link isMissingPackageVersion} 这类程序读。 */
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
- assertNodeVersionSupported(spec, options.nodeVersion);
555
- const processEnv = { ...process.env, ...spec.env, ...env, ...agentEnv };
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 secret of options.stderrSecrets ?? [])
565
- stderrTail.redactSecret(secret);
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
- // Filled in by `bindDraftUpdates` once a session exists. Held in closure variables rather than
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
- app.onNotification('_axom/qoder/profile', (params) => (params ?? {}), (ctx) => {
644
- const params = ctx.params;
645
- const profile = parseQoderProfile(params.profile);
646
- if (!diagnostics.profile || !profile || typeof params.sessionId !== 'string' ||
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
- if (initResponse.protocolVersion !== PROTOCOL_VERSION) {
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
- const sessionCapabilities = initResponse.agentCapabilities?.sessionCapabilities;
787
- diagnostics = { ...diagnostics, profile: parseQoderProfile(initResponse._meta?.qoderProfile) };
788
- const capabilities = {
789
- // Capability markers are empty objects (`{ _meta? }`); supplying `{}` means "supported", and
790
- // omitting the field (or explicit `null`) means "not supported". A truthiness check reads `{}`
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
- `${advertisedAuthMethods.length - authMethods.length} auth method(s) without the required ` +
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
- supportsSteering: steeringAdvertised(initResponse._meta),
850
- steeringTimings: steeringTimings(initResponse._meta),
851
- agentInfo: initResponse.agentInfo ?? undefined,
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
- }