@platless/axom-cli 0.1.0 → 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 (281) hide show
  1. package/README.md +1 -1
  2. package/dist/authScopes.js +19 -0
  3. package/dist/axom-patch/cli/README.md +132 -0
  4. package/dist/axom-patch/cli/account-limits/README.md +27 -0
  5. package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
  6. package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
  7. package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
  8. package/dist/axom-patch/cli/account-limits/generate.py +120 -0
  9. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
  10. package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
  11. package/dist/axom-patch/cli/claude-account-limits.json +37 -0
  12. package/dist/axom-patch/cli/codex-account-limits.json +37 -0
  13. package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
  14. package/dist/axom-patch/cli/nightly-task.md +152 -0
  15. package/dist/axom-patch/cli/pi-steering.json +37 -0
  16. package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
  17. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +38 -0
  18. package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
  19. package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
  20. package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
  21. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +61 -0
  22. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
  23. package/dist/axom-patch/cli/qoder-background-terminal.md +82 -0
  24. package/dist/axom-patch/cli/qoder-foreground-output.md +42 -0
  25. package/dist/axom-patch/cli/qoder-steering.json +52 -0
  26. package/dist/axom-patch/cli/qoder-steering.md +30 -0
  27. package/dist/axom-patch/cli/qoder-subagent-origin.json +73 -0
  28. package/dist/axom-patch/cli/qoder-subagent-subwork.md +73 -0
  29. package/dist/axom-patch/cli/runtime.json +63 -0
  30. package/dist/axom-patch/cli/steering/claude-steering-js.patch +89 -0
  31. package/dist/axom-patch/cli/steering/claude-steering-types.patch +28 -0
  32. package/dist/axom-patch/cli/steering/codex-steering.patch +57 -0
  33. package/dist/axom-patch/cli/steering/pi-steering.js +79 -0
  34. package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
  35. package/dist/axom-patch/cli/zcode-models.json +38 -0
  36. package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
  37. package/dist/cli.js +36 -1
  38. package/dist/commands/auth.js +4 -13
  39. package/dist/commands/execute.js +4 -4
  40. package/dist/commands/record.js +6 -6
  41. package/dist/commands/struct.js +4 -4
  42. package/dist/commands/workstation.js +2 -2
  43. package/dist/credentials.js +2 -2
  44. package/dist/install/claude/commands.js +13 -13
  45. package/dist/keepers/acp/keeper.mjs +3083 -0
  46. package/dist/keepers/acp/manifest.json +9 -0
  47. package/dist/keepers/terminal/keeper.mjs +8469 -0
  48. package/dist/keepers/terminal/manifest.json +9 -0
  49. package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  50. package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  51. package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
  52. package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
  53. package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
  54. package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
  55. package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
  56. package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
  57. package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
  58. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
  59. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
  60. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
  61. package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  62. package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
  63. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  64. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  65. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  66. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  67. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  68. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  69. package/dist/mcp.js +32 -14
  70. package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
  71. package/dist/runtime-deps/qoder-sdk/package.json +8 -0
  72. package/dist/test/keeperKit.js +67 -0
  73. package/dist/workstation/acp/acpAgents.js +2514 -681
  74. package/dist/workstation/acp/acpClient.js +95 -318
  75. package/dist/workstation/acp/activityBusy.js +43 -0
  76. package/dist/workstation/acp/activityReporting.js +54 -0
  77. package/dist/workstation/acp/adoption.js +28 -356
  78. package/dist/workstation/acp/authUrlSniffer.js +36 -0
  79. package/dist/workstation/acp/binaryInstall.js +100 -27
  80. package/dist/workstation/acp/builds/gc.js +245 -0
  81. package/dist/workstation/acp/builds/install/binary.js +28 -0
  82. package/dist/workstation/acp/builds/install/npm.js +235 -0
  83. package/dist/workstation/acp/builds/install/uv.js +254 -0
  84. package/dist/workstation/acp/builds/migrate.js +195 -0
  85. package/dist/workstation/acp/builds/model.js +128 -0
  86. package/dist/workstation/acp/builds/nodeGate.js +100 -0
  87. package/dist/workstation/acp/builds/store.js +352 -0
  88. package/dist/workstation/acp/commandTableProbe.js +68 -0
  89. package/dist/workstation/acp/configOptions.js +103 -1
  90. package/dist/workstation/acp/delegationMcp.js +24 -0
  91. package/dist/workstation/acp/envSecrets.js +27 -0
  92. package/dist/workstation/acp/initializeResponse.js +285 -0
  93. package/dist/workstation/acp/installErrors.js +12 -2
  94. package/dist/workstation/acp/installProbe.js +1 -1
  95. package/dist/workstation/acp/managedPatches.js +144 -0
  96. package/dist/workstation/acp/outbox.js +330 -0
  97. package/dist/workstation/acp/patchAssets.js +80 -0
  98. package/dist/workstation/acp/permission.js +335 -37
  99. package/dist/workstation/acp/processor/checkpoint.js +87 -0
  100. package/dist/workstation/acp/processor/commands.js +35 -0
  101. package/dist/workstation/acp/processor/control.js +355 -0
  102. package/dist/workstation/acp/processor/emit.js +26 -0
  103. package/dist/workstation/acp/processor/jsonrpc.js +172 -0
  104. package/dist/workstation/acp/processor/memoryLog.js +49 -0
  105. package/dist/workstation/acp/processor/origins.js +48 -0
  106. package/dist/workstation/acp/processor/output.js +67 -0
  107. package/dist/workstation/acp/processor/projection.js +42 -0
  108. package/dist/workstation/acp/processor/record.js +128 -0
  109. package/dist/workstation/acp/processor/runtime.js +181 -0
  110. package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
  111. package/dist/workstation/acp/processor/session.js +525 -0
  112. package/dist/workstation/acp/processor/sideChannel.js +80 -0
  113. package/dist/workstation/acp/processor/state.js +74 -0
  114. package/dist/workstation/acp/processor/step.js +459 -0
  115. package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
  116. package/dist/workstation/acp/processor/test/harness.js +120 -0
  117. package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
  118. package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
  119. package/dist/workstation/acp/processor/transport/generations.js +76 -0
  120. package/dist/workstation/acp/processor/transport/keeper.js +130 -0
  121. package/dist/workstation/acp/processor/transport/local.js +269 -0
  122. package/dist/workstation/acp/processor/transport/types.js +78 -0
  123. package/dist/workstation/acp/processor/updates.js +364 -0
  124. package/dist/workstation/acp/processorSession.js +1512 -0
  125. package/dist/workstation/acp/qoderStartup.js +45 -0
  126. package/dist/workstation/acp/registry/catalog.js +385 -4
  127. package/dist/workstation/acp/registry/detect.js +55 -0
  128. package/dist/workstation/acp/registry/launch.js +61 -119
  129. package/dist/workstation/acp/sessionUpdateMapper.js +56 -27
  130. package/dist/workstation/acp/steering.js +58 -0
  131. package/dist/workstation/acp/switching.js +114 -0
  132. package/dist/workstation/acp/terminalLogin.js +171 -0
  133. package/dist/workstation/acp/terminalLoginSpec.js +41 -0
  134. package/dist/workstation/acp/test/adapterFixtures.js +256 -0
  135. package/dist/workstation/acp/test/daemonLife.js +106 -0
  136. package/dist/workstation/acp/test/fakeAgent.js +185 -9
  137. package/dist/workstation/acp/test/keeperWorld.js +267 -0
  138. package/dist/workstation/acp/test/memoryOutbox.js +55 -0
  139. package/dist/workstation/acp/test/raw/fixtures.js +196 -0
  140. package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
  141. package/dist/workstation/acp/test/raw/sanitize.js +344 -0
  142. package/dist/workstation/acp/test/raw/sources.js +249 -0
  143. package/dist/workstation/acp/test/realAdapter.js +377 -0
  144. package/dist/workstation/acp/test/scriptAgent.js +206 -0
  145. package/dist/workstation/acp/test/sessionFrames.js +42 -0
  146. package/dist/workstation/acp/timedCommand.js +4 -2
  147. package/dist/workstation/acp/uvxInstall.js +72 -122
  148. package/dist/workstation/acp/validate.js +33 -5
  149. package/dist/workstation/agentFlow.js +103 -0
  150. package/dist/workstation/agentSessions.js +1686 -348
  151. package/dist/workstation/channels/agent.js +14 -6
  152. package/dist/workstation/channels/computer.js +203 -7
  153. package/dist/workstation/channels/computerExecs.js +200 -0
  154. package/dist/workstation/channels/management.js +86 -6
  155. package/dist/workstation/config.js +3 -1
  156. package/dist/workstation/connection/encryptedChannel.js +88 -21
  157. package/dist/workstation/daemon.js +309 -6
  158. package/dist/workstation/daemonctl.js +28 -2
  159. package/dist/workstation/desktop/binding.js +36 -0
  160. package/dist/workstation/desktop/bridge.js +212 -0
  161. package/dist/workstation/desktop/flow-control.js +52 -0
  162. package/dist/workstation/desktop/focused-text.js +130 -0
  163. package/dist/workstation/desktop/protocol.js +35 -0
  164. package/dist/workstation/exec/attach.js +126 -0
  165. package/dist/workstation/exec/command.js +40 -0
  166. package/dist/workstation/exec/durable.js +517 -0
  167. package/dist/workstation/exec/fallback.js +106 -0
  168. package/dist/workstation/exec/policy.js +837 -0
  169. package/dist/workstation/exec/shaping.js +211 -0
  170. package/dist/workstation/exec.js +3 -112
  171. package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
  172. package/dist/workstation/files/adapters/toolRequests.js +121 -0
  173. package/dist/workstation/files/client.js +455 -0
  174. package/dist/workstation/files/errors.js +28 -0
  175. package/dist/workstation/files/fence.js +350 -0
  176. package/dist/workstation/files/find/fileIndex.js +153 -0
  177. package/dist/workstation/files/find/fuzzy.js +155 -0
  178. package/dist/workstation/files/host.js +122 -0
  179. package/dist/workstation/files/hostProtocol.js +2 -0
  180. package/dist/workstation/files/media/cache.js +620 -0
  181. package/dist/workstation/files/media/encoding.js +308 -0
  182. package/dist/workstation/files/media/estimator.js +100 -0
  183. package/dist/workstation/files/media/media.js +325 -0
  184. package/dist/workstation/files/media/packedAudio.js +23 -0
  185. package/dist/workstation/files/media/scheduler.js +551 -0
  186. package/dist/workstation/files/media/types.js +5 -0
  187. package/dist/workstation/files/scope.js +45 -0
  188. package/dist/workstation/files/structured.js +284 -0
  189. package/dist/workstation/files/text.js +356 -0
  190. package/dist/workstation/files/uploads.js +351 -0
  191. package/dist/workstation/journal.js +65 -236
  192. package/dist/workstation/keepers/acp/client.js +101 -0
  193. package/dist/workstation/keepers/acp/envelope.js +62 -0
  194. package/dist/workstation/keepers/acp/host.js +419 -0
  195. package/dist/workstation/keepers/acp/main.js +18 -0
  196. package/dist/workstation/keepers/acp/memoryStream.js +121 -0
  197. package/dist/workstation/keepers/acp/params.js +137 -0
  198. package/dist/workstation/keepers/acp/protocol.js +66 -0
  199. package/dist/workstation/keepers/acp/recover.js +120 -0
  200. package/dist/workstation/keepers/acp/session.js +584 -0
  201. package/dist/workstation/keepers/acp/stderr.js +154 -0
  202. package/dist/workstation/keepers/kit/activity.js +103 -0
  203. package/dist/workstation/keepers/kit/bootstrap.js +164 -0
  204. package/dist/workstation/keepers/kit/buildVersion.js +51 -0
  205. package/dist/workstation/keepers/kit/bundle.js +120 -0
  206. package/dist/workstation/keepers/kit/client.js +432 -0
  207. package/dist/workstation/keepers/kit/identity.js +21 -0
  208. package/dist/workstation/keepers/kit/ipc.js +112 -0
  209. package/dist/workstation/keepers/kit/launchConfig.js +8 -0
  210. package/dist/workstation/keepers/kit/launcher.js +249 -0
  211. package/dist/workstation/keepers/kit/lock.js +123 -0
  212. package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
  213. package/dist/workstation/keepers/kit/paths.js +75 -0
  214. package/dist/workstation/keepers/kit/protocol.js +43 -0
  215. package/dist/workstation/keepers/kit/server.js +518 -0
  216. package/dist/workstation/keepers/kit/streamLog.js +697 -0
  217. package/dist/workstation/keepers/legacy.js +296 -0
  218. package/dist/workstation/keepers/policy.js +576 -0
  219. package/dist/workstation/keepers/stop.js +34 -0
  220. package/dist/workstation/keepers/terminal/client.js +213 -0
  221. package/dist/workstation/keepers/terminal/exec.js +981 -0
  222. package/dist/workstation/keepers/terminal/generations.js +129 -0
  223. package/dist/workstation/keepers/terminal/keeper.js +62 -0
  224. package/dist/workstation/keepers/terminal/main.js +43 -0
  225. package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
  226. package/dist/workstation/keepers/terminal/params.js +27 -0
  227. package/dist/workstation/keepers/terminal/protocol.js +56 -0
  228. package/dist/workstation/keepers/terminal/pty.js +330 -0
  229. package/dist/workstation/oauth/deviceAuthorization.js +21 -12
  230. package/dist/workstation/protocol/accountLimits.js +47 -0
  231. package/dist/workstation/protocol/agent.js +201 -8
  232. package/dist/workstation/protocol/common.js +36 -0
  233. package/dist/workstation/protocol/computer.js +107 -7
  234. package/dist/workstation/protocol/management.js +50 -2
  235. package/dist/workstation/qoderAdapter/backgroundProcess.js +118 -0
  236. package/dist/workstation/qoderAdapter/backgroundTerminal.js +193 -0
  237. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
  238. package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
  239. package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
  240. package/dist/workstation/qoderAdapter/contract.js +78 -0
  241. package/dist/workstation/qoderAdapter/foregroundOutput.js +36 -0
  242. package/dist/workstation/qoderAdapter/history.js +85 -0
  243. package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
  244. package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
  245. package/dist/workstation/qoderAdapter/main.js +95 -0
  246. package/dist/workstation/qoderAdapter/mapper.js +117 -0
  247. package/dist/workstation/qoderAdapter/observerPlugin.js +245 -0
  248. package/dist/workstation/qoderAdapter/observerReceiver.js +187 -0
  249. package/dist/workstation/qoderAdapter/permissions.js +61 -0
  250. package/dist/workstation/qoderAdapter/profile.js +93 -0
  251. package/dist/workstation/qoderAdapter/sdkDriver.js +48 -0
  252. package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
  253. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
  254. package/dist/workstation/qoderAdapter/session.js +319 -0
  255. package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
  256. package/dist/workstation/qoderAdapter/subwork.js +85 -0
  257. package/dist/workstation/qoderAdapter/taskControl.js +38 -0
  258. package/dist/workstation/qoderAdapter/taskLedger.js +163 -0
  259. package/dist/workstation/qoderAdapter/types.js +1 -0
  260. package/dist/workstation/qoderAdapter/usage.js +37 -0
  261. package/dist/workstation/register.js +4 -0
  262. package/dist/workstation/systemMetrics.js +110 -1
  263. package/dist/workstation/tools/browser.js +394 -0
  264. package/dist/workstation/tools/client.js +277 -0
  265. package/dist/workstation/tools/info.js +50 -0
  266. package/dist/workstation/tools/keeperProcess.js +124 -0
  267. package/dist/workstation/tools/router.js +55 -0
  268. package/dist/workstation/tools/terminalRouter.js +222 -0
  269. package/dist/workstation/tools/validation.js +86 -0
  270. package/dist/workstation/update/coordinator.js +32 -0
  271. package/dist/workstation/update/npmInstaller.js +2 -0
  272. package/dist/workstation/update/ptyPreflight.js +45 -0
  273. package/dist/workstation/update/taskRegistry.js +141 -23
  274. package/dist/workstation/update/watchdog.js +66 -0
  275. package/package.json +20 -7
  276. package/skills/axom/SKILL.md +55 -17
  277. package/skills/axom-definition/SKILL.md +32 -32
  278. package/skills/axom-platform/SKILL.md +39 -39
  279. package/dist/workstation/acp/acpSession.js +0 -1047
  280. package/dist/workstation/acp/contract.js +0 -494
  281. package/dist/workstation/acp/draftInterceptor.js +0 -154
@@ -3,10 +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 { airCapabilityMeta, asSdkClientCapabilities, AIR_ASYNC_TASKS_CAPABILITY, AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY, DRAFT_SUBAGENTS_CAPABILITY, } from './draftSubagents.js';
7
- import { interceptDraftUpdates } from './draftInterceptor.js';
8
- export class UnsupportedNodeVersionError extends Error {
9
- }
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';
10
13
  /**
11
14
  * A subprocess failed to start, or died before it finished responding to `initialize`.
12
15
  *
@@ -14,8 +17,7 @@ export class UnsupportedNodeVersionError extends Error {
14
17
  * 上写的那几行 —— npx 的 `npm error code ETARGET`、adapter 的崩溃栈、代理的拒绝。这里以前是
15
18
  * `stdio: 'inherit'`,那几行只落进 daemon.log,调用方拿到的是一句
16
19
  * "exited before responding to initialize (code=1)",服务端那头再转手就成了一条内容为空的
17
- * "ACP connection closed" —— 排查的人手上什么都没有。见 {@link isMissingPackageVersion}:那条判定
18
- * 需要的是证据,不是从「管子断了」倒推出来的猜测。
20
+ * "ACP connection closed" —— 排查的人手上什么都没有。
19
21
  */
20
22
  export class AgentSpawnError extends Error {
21
23
  stderr;
@@ -45,156 +47,15 @@ export class AgentInitializeTimeoutError extends Error {
45
47
  */
46
48
  const NPM_ETARGET = /(?:^|[^A-Z])ETARGET(?![A-Z])/;
47
49
  /**
48
- * 判定规则本身,脱离 {@link AgentSpawnError}/{@link AgentInitializeTimeoutError} 单独导出 ——
49
- * `installNpxPackage`(`installProbe.ts`)是这条规则的第二个读者:它下载适配器失败时报的是一段
50
- * `InstallOutcome.error`/`stderr` 字符串,不是这两个类里的一个实例,没有 `.stderr` 字段可供
51
- * {@link isMissingPackageVersion} 去 `instanceof` 窄化。两个调用点都要认同一件事——npm 自己打在
52
- * stderr 上的 `ETARGET` 错误码——所以正则只在这一处,两个调用点都直接测字符串,而不是各自维护一份。
50
+ * 判定规则本身:npm 自己打在 stderr 上的 `ETARGET` 错误码。读者是安装期的回退
51
+ * (`AcpAgents.installPristineWithFallback`)——启动期不再有回退,启动从不解析版本(spec 2026-10-11 §6.2)。
53
52
  */
54
53
  export function stderrShowsMissingPackageVersion(stderr) {
55
54
  return NPM_ETARGET.test(stderr);
56
55
  }
57
- /**
58
- * 这次启动失败,是不是因为 registry 钉的版本 npm 根本没有。
59
- *
60
- * 只看 {@link AgentSpawnError.stderr} / {@link AgentInitializeTimeoutError.stderr} 里的原始文本 ——
61
- * 超时也算进来是因为实测过:npx 在一台慢机器上可以把 20s 的 initialize 预算耗光之后才让子进程死掉,
62
- * 那时候 ETARGET 早就写在 stderr 上了,只是赢下 race 的是计时器。
63
- */
64
- export function isMissingPackageVersion(error) {
65
- if (!(error instanceof AgentSpawnError) && !(error instanceof AgentInitializeTimeoutError))
66
- return false;
67
- return stderrShowsMissingPackageVersion(error.stderr);
68
- }
69
56
  /** The agent negotiated a protocol version this client does not speak. */
70
57
  export class UnsupportedProtocolVersionError extends Error {
71
58
  }
72
- /**
73
- * Minimum Node.js major version a known agent package requires, keyed by its npx package name
74
- * (unversioned — matched against both a bare specifier and a version-pinned one; see
75
- * `argMatchesPackage`).
76
- *
77
- * The registry has no field for this — verified fact, not an oversight to fix here. The only place
78
- * a Node requirement is declared is the agent package's own `package.json#engines`, and `npx` does
79
- * not enforce it: it just runs the package under whatever `node` is already on PATH. Getting this
80
- * wrong means the user sees a subprocess crash stack instead of an actionable message (ruling R7),
81
- * so this table exists to catch the one adapter known, as of 2026-08-21, to need it.
82
- */
83
- const KNOWN_MIN_NODE_MAJOR = [
84
- { packageName: '@agentclientprotocol/claude-agent-acp', minMajor: 22 },
85
- ];
86
- /**
87
- * True when `arg` is exactly `packageName`, or `packageName` followed by an `@version` suffix.
88
- * `arg.startsWith(packageName)` alone would also match an unrelated package that merely shares a
89
- * prefix (e.g. `@agentclientprotocol/claude-agent-acp-extra`); anchoring on an exact match or an
90
- * `@` boundary rules that out while still matching ruling R3's user-supplied, unpinned
91
- * `command`/`args` override, which has no version suffix to key off at all.
92
- */
93
- function argMatchesPackage(arg, packageName) {
94
- return arg === packageName || arg.startsWith(`${packageName}@`);
95
- }
96
- /** Exported for tests: derives the minimum Node major version a launch spec requires, if known. */
97
- export function requiredNodeMajorFor(spec) {
98
- const match = KNOWN_MIN_NODE_MAJOR.find((entry) => spec.args.some((arg) => argMatchesPackage(arg, entry.packageName)));
99
- return match?.minMajor;
100
- }
101
- /**
102
- * Throws a message a user can act on when this machine's Node is older than a known agent
103
- * requires. `nodeVersion` defaults to the real running Node; it takes an explicit override so tests
104
- * can exercise both sides of the gate without depending on the host's actual Node version —
105
- * `process.versions.node` is a frozen property and cannot be stubbed.
106
- */
107
- export function assertNodeVersionSupported(spec, nodeVersion = process.versions.node) {
108
- const required = requiredNodeMajorFor(spec);
109
- if (required === undefined)
110
- return;
111
- const currentMajor = Number.parseInt(nodeVersion.split('.')[0] ?? '', 10);
112
- if (Number.isNaN(currentMajor) || currentMajor < required) {
113
- throw new UnsupportedNodeVersionError(`This agent requires Node.js >= ${required}, but this machine is running Node.js ${nodeVersion}. ` +
114
- 'Install a newer Node.js and try again.');
115
- }
116
- }
117
- /**
118
- * Whether an `InitializeResponse._meta` advertises the `_session/steering` extension.
119
- *
120
- * ## `_session/steering` is not ACP
121
- *
122
- * It is absent from the schema this client is pinned to (`grep steer
123
- * node_modules/@agentclientprotocol/sdk/dist/schema/types.gen.d.ts` returns nothing). It is an
124
- * extension `claude-agent-acp` implements: the method constant lives at its `acp-agent.js@0.76.0:164`
125
- * and its own comment calls it a *"Custom (extension) request method ... Named `_session/steering`
126
- * per the agreed ACP steering wire protocol"*. The leading underscore is ACP's extension namespace.
127
- *
128
- * ## Why the top-level `_meta` specifically
129
- *
130
- * The adapter advertises it at `InitializeResponse._meta.steering.supported`, a **sibling of
131
- * `agentCapabilities`** (`acp-agent.js@0.76.0:944-950`), not a key inside it. `_meta` is typed
132
- * `{[key: string]: unknown} | null` (`types.gen.d.ts:1498-1500`), so every hop down has to be
133
- * narrowed rather than indexed through a cast.
134
- *
135
- * Strictly `=== true`. An agent that sends `steering: {}` (the shape ACP uses for its *own*
136
- * capability markers) has not said `supported`, and this extension does not define that spelling —
137
- * reading it as a yes would be this client inventing a wire contract.
138
- */
139
- export function steeringAdvertised(meta) {
140
- if (typeof meta !== 'object' || meta === null)
141
- return false;
142
- const steering = meta.steering;
143
- if (typeof steering !== 'object' || steering === null)
144
- return false;
145
- return steering.supported === true;
146
- }
147
- /**
148
- * The delivery timings the `_session/steering` extension defines — when the agent hands an injected
149
- * message to the model. `next` is the only lossless one: `now` aborts the running generation and
150
- * discards whatever tool calls were still outstanding.
151
- */
152
- export const STEER_TIMINGS = ['now', 'next', 'later'];
153
- /**
154
- * Which delivery timings an `InitializeResponse._meta` says the agent accepts —
155
- * `_meta.steering.timings`, the sibling of the `supported` flag {@link steeringAdvertised} reads,
156
- * and read from the **top-level** `_meta` for exactly the same reason.
157
- *
158
- * ## Empty is the answer, not the absence of one
159
- *
160
- * An agent that declares no timings has told us we may not name one. That is not "assume the
161
- * default" — the extension ignores an unrecognised `priority` **silently** (the measurement behind
162
- * ruling N4: sending `next` to today's adapters lands as `now`, which throws away the user's
163
- * in-flight work). So an empty result must reach the caller as "do not specify a timing at all",
164
- * and today — before the upstream PR that makes adapters broadcast this — every agent in the wild
165
- * produces exactly that. Steady state, not a bug.
166
- *
167
- * ## Unknown values are dropped, never rejected
168
- *
169
- * A timing this build has never heard of is skipped and the known ones beside it survive. This
170
- * client is deployed on user machines and can be older than both the agent it launches and the
171
- * server it reports to; throwing (or discarding the whole array) on a value from a future version
172
- * of the extension would take steering away from an agent that does support it. Non-strings are
173
- * dropped the same way: `_meta` is typed `{[key: string]: unknown}`, so nothing here is guaranteed.
174
- *
175
- * Duplicates collapse — the wire shape is a JSON array but the fact is a set.
176
- */
177
- export function steeringTimings(meta) {
178
- if (typeof meta !== 'object' || meta === null)
179
- return [];
180
- const steering = meta.steering;
181
- if (typeof steering !== 'object' || steering === null)
182
- return [];
183
- const declared = steering.timings;
184
- if (!Array.isArray(declared))
185
- return [];
186
- const known = [];
187
- for (const entry of declared) {
188
- if (typeof entry !== 'string')
189
- continue;
190
- if (!STEER_TIMINGS.includes(entry))
191
- continue;
192
- if (known.includes(entry))
193
- continue;
194
- known.push(entry);
195
- }
196
- return known;
197
- }
198
59
  /**
199
60
  * The JSON-RPC error code an agent answered with, or `undefined` when the failure was not one an
200
61
  * agent answered at all.
@@ -225,10 +86,7 @@ export function steeringTimings(meta) {
225
86
  export function jsonRpcErrorCode(error) {
226
87
  if (!(error instanceof RequestError))
227
88
  return undefined;
228
- const { code } = error;
229
- if (!Number.isInteger(code))
230
- return undefined;
231
- return code >= -2_147_483_648 && code <= 2_147_483_647 ? code : undefined;
89
+ return jsonRpcErrorCodeOf(error.code);
232
90
  }
233
91
  function translateSpawnError(command, error, stderr) {
234
92
  if (error.code === 'ENOENT') {
@@ -251,7 +109,7 @@ const STDERR_TAIL_LIMIT = 8_192;
251
109
  */
252
110
  const STDERR_FLUSH_GRACE_MS = 250;
253
111
  /**
254
- * 把子进程的 stderr 接出来:一边原样转发到本进程的 stderr,一边留一份有界的尾巴给启动期的报错带上,
112
+ * 把子进程的 stderr 接出来:普通连接原样转发,托管 MCP 请求后的输出逐行脱敏,并留一份有界的安全尾巴给报错带上,
255
113
  * 一边逐行喂给 {@link sniffAuthUrl} —— 有的 agent(实测 antigravity)不走 `elicitation/create` 递
256
114
  * 登录链接,而是直接打到 stderr 上(见 `authUrlSniffer.ts` 的文件头注释)。
257
115
  *
@@ -269,9 +127,41 @@ const STDERR_FLUSH_GRACE_MS = 250;
269
127
  */
270
128
  function captureChildStderr(stream, present) {
271
129
  if (!stream)
272
- return { text: () => '', flushed: () => Promise.resolve() };
130
+ return { text: () => '', flushed: () => Promise.resolve(), redactValue: () => { } };
273
131
  let tail = '';
274
132
  let lineBuffer = '';
133
+ let protectedLine = '';
134
+ let droppingLongLine = false;
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
+ };
143
+ const forward = (value) => {
144
+ tail = (tail + value).slice(-STDERR_TAIL_LIMIT);
145
+ process.stderr.write(value);
146
+ };
147
+ const forwardProtected = (value) => {
148
+ protectedLine += value;
149
+ let newline;
150
+ while ((newline = protectedLine.indexOf('\n')) >= 0) {
151
+ if (droppingLongLine) {
152
+ droppingLongLine = false;
153
+ }
154
+ else {
155
+ forward(redactAll(protectedLine.slice(0, newline + 1)));
156
+ }
157
+ protectedLine = protectedLine.slice(newline + 1);
158
+ }
159
+ if (protectedLine.length > STDERR_TAIL_LIMIT) {
160
+ protectedLine = '';
161
+ droppingLongLine = true;
162
+ forward('[acp] child stderr line omitted\n');
163
+ }
164
+ };
275
165
  // One login can print its own URL more than once — the ordinary shape of an OAuth CLI is "here's
276
166
  // the link" immediately followed by "if your browser didn't open, here it is again", verbatim
277
167
  // the same URL. Without this, that becomes two permanent, identical, un-retractable cards for a
@@ -315,19 +205,36 @@ function captureChildStderr(stream, present) {
315
205
  };
316
206
  stream.on('data', (chunk) => {
317
207
  const text = chunk.toString();
318
- tail = (tail + text).slice(-STDERR_TAIL_LIMIT);
319
- process.stderr.write(chunk);
320
- scanForAuthUrl(text);
208
+ if (quiet.size > 0) {
209
+ forwardProtected(text);
210
+ scanForAuthUrl(text);
211
+ }
212
+ else {
213
+ tail = (tail + text).slice(-STDERR_TAIL_LIMIT);
214
+ process.stderr.write(chunk);
215
+ scanForAuthUrl(text);
216
+ }
321
217
  });
322
218
  // 读端出错(SIGKILL 掉子进程时管子会断)没有谁需要处理,但一个没有监听器的 `'error'` 会把整个
323
219
  // 守护进程掀翻 —— 这行不是防御性代码,是 EventEmitter 的硬性要求。
324
220
  stream.on('error', () => { });
325
221
  // A final unterminated line (no trailing `\n` before the process exits) is still a real line —
326
222
  // scan whatever is left in the buffer once the stream itself says there is no more coming.
327
- void ended.then(() => scanForAuthUrl('\n'));
223
+ void ended.then(() => {
224
+ if (protectedLine && !droppingLongLine)
225
+ forward(redactAll(protectedLine));
226
+ scanForAuthUrl('\n');
227
+ });
328
228
  return {
329
229
  text: () => tail,
330
230
  flushed: (ms) => settledWithin(ended, ms).then(() => undefined),
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);
237
+ },
331
238
  };
332
239
  }
333
240
  /**
@@ -350,7 +257,7 @@ function rejectionWithin(failure, ms) {
350
257
  });
351
258
  });
352
259
  }
353
- /** 报错正文里只放最后几行;完整尾巴留在 `.stderr` 上给 {@link isMissingPackageVersion} 这类程序读。 */
260
+ /** 报错正文里只放最后几行;完整尾巴留在 `.stderr` 上给程序读。 */
354
261
  function lastStderrLines(text, count) {
355
262
  return text.split('\n').map((line) => line.trim()).filter(Boolean).slice(-count).join(' | ');
356
263
  }
@@ -451,94 +358,41 @@ export async function closeAgentTransport(child, connection, graceMs = CHILD_EXI
451
358
  connection.close();
452
359
  await connection.closed.catch(() => { });
453
360
  }
454
- /**
455
- * One `initialize` auth method, or `null` when the agent did not advertise a usable one.
456
- *
457
- * `initResponse` is another process's JSON that arrived over a pipe; `AuthMethod` is what this
458
- * client *asked* for, not what it was handed. Both variants of that union make `id` and `name`
459
- * required (`@agentclientprotocol/sdk` `types.gen.d.ts:2241-2246` for `AuthMethodTerminal`,
460
- * `:2281-2286` for `AuthMethodAgent`), so an agent that omits them is off-spec -- but nothing
461
- * between its stdout and here checks, and the snapshot then carries `undefined` in two positions
462
- * its own type declares non-optional. That lie does not stay local: `JSON.stringify` erases an
463
- * `undefined` value entirely, so the object passes every `typeof x.id` check in this process and
464
- * still reaches a peer with the keys missing.
465
- *
466
- * `agoragentic-acp` advertises exactly `{type: 'terminal', description: '…'}`. Observing it once
467
- * put a malformed `AuthMethodInfo` in the roster, which rides in every subsequent
468
- * `management_hello`, which the server then cannot decode -- and a hello the server cannot decode
469
- * closes the socket with nothing said, so the workstation retried into a permanent OFFLINE.
470
- *
471
- * Dropping rather than repairing: `id` is the value `authenticate` is called with, so inventing
472
- * one would trade a missing sign-in method for a broken one.
473
- */
474
- function toAuthMethodSnapshot(method) {
475
- if (typeof method.id !== 'string' || typeof method.name !== 'string')
476
- return null;
477
- return {
478
- id: method.id,
479
- name: method.name,
480
- ...('type' in method && method.type === 'terminal' ? { type: 'terminal' } : {}),
481
- ...(method.description != null ? { description: method.description } : {}),
482
- // `args` only exists on `AuthMethodTerminal` — an `agent`-type method has no such field, and
483
- // `'args' in method` is false for it, which is exactly "nothing to show" (see `AgentAuthMethod`).
484
- ...('args' in method && Array.isArray(method.args) ? { args: method.args } : {}),
485
- };
486
- }
487
361
  const DEFAULT_INITIALIZE_TIMEOUT_MS = 30_000;
488
362
  /**
489
363
  * Spawns an ACP agent subprocess, connects to it over ndjson stdio, and negotiates capabilities
490
364
  * via `initialize`. `env` and `agentEnv` are overlay layers on top of the current process
491
365
  * environment, not full environments (existing contract carried over from the deleted bridge
492
366
  * transport): if `PATH` is present in either, it has already been resolved by the caller and must
493
- * 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.
494
369
  */
495
370
  export async function connectAgent(spec, env, agentEnv, options = {}) {
496
- assertNodeVersionSupported(spec, options.nodeVersion);
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);
497
375
  const child = spawn(spec.command, spec.args, {
498
376
  // stderr 是 `pipe` 而不是 `inherit`,而且**依然不接进协议流** —— 它被 `captureChildStderr`
499
377
  // 单独读走:原样转发到 daemon.log(`inherit` 以前免费提供的那件事),同时留一份尾巴给启动期的
500
378
  // 报错。启动失败时 npm 写在这里的文字是唯一的一手证据。
501
379
  stdio: ['pipe', 'pipe', 'pipe'],
502
- env: { ...process.env, ...spec.env, ...env, ...agentEnv },
380
+ env: processEnv,
503
381
  });
504
382
  const stderrTail = captureChildStderr(child.stderr, options.present);
383
+ for (const value of overlayEnvSecrets({ env, agentEnv }))
384
+ stderrTail.redactValue(value);
505
385
  const lifecycle = watchChildLifecycle(child, spec.command, stderrTail);
506
386
  // Argument order reads backwards from what the names suggest: argument 1 is the child's stdin
507
387
  // (we write to it), argument 2 is the child's stdout (we read from it). Swapping them still
508
388
  // compiles and still "connects" — the initialize request just never arrives, so the failure mode
509
389
  // is a hang, not an error.
510
- // Filled in by `bindDraftUpdates` once a session exists. Held in closure variables rather than
511
- // passed in, because the interceptor has to be in place before `initialize` — the agent may send
512
- // its first update before anyone has had a chance to register anything.
513
- let ownSessionId = null;
514
- let draftSink = null;
515
- // Sessions this connection handles by some route other than an `AcpSession` — today only
516
- // adoption's replay. Kept beside `ownSessionId` rather than folded into it because they mean
517
- // different things: one is "the session we drive", these are "sessions someone else on this
518
- // connection is already reading".
519
- const knownSessionIds = new Set();
520
- const stream = ndJsonStream(Writable.toWeb(child.stdin),
521
- // The child's stdout, minus the notifications the pinned SDK cannot deliver. Everything else
522
- // reaches it byte-for-byte; see `draftInterceptor.ts` for the measurements behind this.
523
- interceptDraftUpdates(Readable.toWeb(child.stdout), {
524
- // Unbound means unknown, and unknown must not be claimed as ours: treating a session we have
525
- // not been told about as our own would send its frames to the SDK, which drops them silently.
526
- // Diverting is the recoverable direction — the sink can decide, and it can log.
527
- isOwnSession: (sessionId) => (ownSessionId !== null && sessionId === ownSessionId) || knownSessionIds.has(sessionId),
528
- onDiverted: (update) => {
529
- if (draftSink) {
530
- draftSink(update);
531
- return;
532
- }
533
- // No sink yet: say so once per update rather than dropping in silence. This is reachable
534
- // only before the session is bound, which is before any prompt, so it should stay rare —
535
- // and if it stops being rare, that is worth seeing.
536
- console.warn(`[acp] dropped a ${update.reason} session/update for '${update.sessionId}': ` +
537
- 'no session is bound to this connection yet');
538
- },
539
- }));
390
+ const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
540
391
  const app = client({ name: 'axom' });
541
- 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 });
542
396
  const connection = app.connect(stream);
543
397
  const timeoutMs = options.initializeTimeoutMs ?? DEFAULT_INITIALIZE_TIMEOUT_MS;
544
398
  let timeoutHandle;
@@ -555,53 +409,7 @@ export async function connectAgent(spec, env, agentEnv, options = {}) {
555
409
  let initResponse;
556
410
  try {
557
411
  initResponse = await Promise.race([
558
- connection.agent.request('initialize', {
559
- protocolVersion: PROTOCOL_VERSION,
560
- // Three things are declared here, and they are not interchangeable — see
561
- // `draftSubagents.ts` for the tier each one sits in.
562
- //
563
- // 1. `subagents: {}` is the **canonical** draft field from ACP RFD #1992. Both official
564
- // adapters gate native subagent sessions on it, and both answer with
565
- // `agentCapabilities.sessionCapabilities.subagents` (measured against each).
566
- //
567
- // 2. The AIR list is sent as well, for two separate reasons. It is the *only* channel for
568
- // `asyncTasks` — nothing canonical exists for background work — and it is the fallback
569
- // for `nativeSubagentSessions`, because a released SDK sitting between us and an agent
570
- // can strip a draft field it has no type for (upstream says so in #1017's compatibility
571
- // note). Declaring both is not belt-and-braces: they cover different failure modes.
572
- //
573
- // 3. `subagent-transcript: true` stays. It is claude-agent-acp's older vendor key
574
- // (`dist/acp-agent.js:155-158`), and it still governs the **fallback** path taken when
575
- // native subagent sessions are not negotiated — an older adapter, or a stripped
576
- // capability. Without it that path strips every subagent's text and thinking
577
- // (`stripSubagentTextAndThinking`, `:3926`), leaving nothing but tool-call wreckage.
578
- // Adapters that have never heard of any of these keys are required by the spec to
579
- // ignore them.
580
- clientCapabilities: asSdkClientCapabilities({
581
- [DRAFT_SUBAGENTS_CAPABILITY]: {},
582
- // `ClientSessionCapabilities.configOptions` (`types.gen.d.ts:4306`), part of the schema
583
- // proper — unlike `subagents` above, this survives the pinned SDK's `zInitializeRequest`
584
- // stripping because it's a known field, not a draft one. `{}` for `boolean` tells the
585
- // agent it may send `type: "boolean"` entries in `SessionConfigOption` and that this
586
- // client will send `session/set_config_option` with a boolean `value`
587
- // (`SessionConfigOptionsCapabilities`, `types.gen.d.ts:4333-4353`); without it an agent
588
- // with a boolean toggle downgrades it to a two-value select.
589
- session: { configOptions: { boolean: {} } },
590
- // `ClientCapabilities.elicitation` (`types.gen.d.ts:4479`) is a known field of the schema
591
- // proper, not a draft one, so it survives `zInitializeRequest` stripping as-is. `{}` for
592
- // `url` (`ElicitationUrlCapabilities`, `:4616`) means the client can put a URL in front of
593
- // the user; this is the precondition for the whole device-code login shape to exist at all
594
- // — Codex's own `getCodexAuthMethods` gates its `chat-gpt-device-code` auth method on
595
- // `clientCapabilities?.elicitation?.url != null`. Without declaring it, Codex never offers
596
- // that method and `elicitation/create` with `mode: 'url'` never arrives to answer (see
597
- // `permission.ts`'s `handleCreateElicitation`).
598
- elicitation: { url: {} },
599
- _meta: {
600
- 'subagent-transcript': true,
601
- ...airCapabilityMeta(AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY, AIR_ASYNC_TASKS_CAPABILITY),
602
- },
603
- }),
604
- }),
412
+ connection.agent.request('initialize', initializeRequestParams()),
605
413
  lifecycle.failure,
606
414
  timeout,
607
415
  ]);
@@ -622,68 +430,37 @@ export async function connectAgent(spec, env, agentEnv, options = {}) {
622
430
  }
623
431
  // Global Constraint 1 is v1-only; the schema calls for disconnecting when the agent didn't
624
432
  // accept the version this client sent.
625
- if (initResponse.protocolVersion !== PROTOCOL_VERSION) {
433
+ const reading = parseInitializeResult(initResponse);
434
+ if (!reading.protocolSupported) {
626
435
  const error = new UnsupportedProtocolVersionError(`Agent '${spec.command}' negotiated protocol version ${initResponse.protocolVersion}, ` +
627
436
  `but this client only supports version ${PROTOCOL_VERSION}.`);
628
437
  connection.close(error);
629
438
  child.kill();
630
439
  throw error;
631
440
  }
632
- const sessionCapabilities = initResponse.agentCapabilities?.sessionCapabilities;
633
- const capabilities = {
634
- // Capability markers are empty objects (`{ _meta? }`); supplying `{}` means "supported", and
635
- // omitting the field (or explicit `null`) means "not supported". A truthiness check reads `{}`
636
- // correctly by accident but reads `null`/`undefined` correctly too, which hides the real bug:
637
- // this must stay an explicit `!= null`, never `Boolean(...)` or a bare truthy test.
638
- list: sessionCapabilities?.list != null,
639
- resume: sessionCapabilities?.resume != null,
640
- loadSession: initResponse.agentCapabilities?.loadSession === true,
641
- logout: initResponse.agentCapabilities?.auth?.logout != null,
642
- /**
643
- * ACP `PromptCapabilities.image` (`types.gen.d.ts:1601`): "Agent supports
644
- * [`ContentBlock::Image`]". Every field of that type is optional, so an agent that says
645
- * nothing supports text only -- `=== true` rather than `!== false`.
646
- *
647
- * Verified against codex-acp 1.7.0, which declares `{embeddedContext: true, image: true}`.
648
- * `audio` is deliberately not read: no agent we target declares it, and an unread capability
649
- * is a field nobody can act on.
650
- */
651
- promptImage: initResponse.agentCapabilities?.promptCapabilities?.image === true,
652
- };
653
- // Honour the advertised capability, not what the agent happens to route: some adapters (e.g.
654
- // codex-acp routing `session/fork`) implement more than they declare in `agentCapabilities`.
655
- // Reading `capabilities` above off `initResponse` — never probing the agent for what it accepts
656
- // — is what keeps that distinction intact.
657
- const advertisedAuthMethods = initResponse.authMethods ?? [];
658
- const authMethods = advertisedAuthMethods
659
- .map(toAuthMethodSnapshot)
660
- .filter((method) => method !== null);
661
- 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) {
662
446
  // Named and counted rather than dropped in silence: the user loses a sign-in method they can
663
447
  // see advertised in the agent's own docs, and the only honest answer to "why" is which agent
664
448
  // sent what. `agentInfo.name` first because most agents launch through `npx`/`uvx` and
665
449
  // `spec.command` would then name the launcher, which identifies none of the ~40 registry
666
450
  // entries. See `toAuthMethodSnapshot` for what accepting these costs instead.
667
451
  console.warn(`[acp] agent '${initResponse.agentInfo?.name ?? spec.command}' advertised ` +
668
- `${advertisedAuthMethods.length - authMethods.length} auth method(s) without the required ` +
452
+ `${droppedAuthMethods} auth method(s) without the required ` +
669
453
  'id/name; ignoring them');
670
454
  }
671
455
  return {
672
456
  capabilities,
673
457
  authMethods,
674
- supportsSteering: steeringAdvertised(initResponse._meta),
675
- steeringTimings: steeringTimings(initResponse._meta),
676
- agentInfo: initResponse.agentInfo ?? undefined,
458
+ terminalLogins,
459
+ supportsAccountLimits: reading.supportsAccountLimits,
460
+ supportsSteering: reading.supportsSteering,
461
+ steeringTimings: reading.steeringTimings,
462
+ agentInfo: reading.agentInfo,
677
463
  connection,
678
- agentProcessAlive: () => child.exitCode === null && child.signalCode === null,
679
- bindDraftUpdates(sessionId, sink) {
680
- ownSessionId = sessionId;
681
- draftSink = sink;
682
- },
683
- markSessionKnown(sessionId) {
684
- knownSessionIds.add(sessionId);
685
- return () => knownSessionIds.delete(sessionId);
686
- },
687
464
  // Child first, streams second — see `closeAgentTransport` for why that order is the fix and not
688
465
  // an incidental detail.
689
466
  close: () => closeAgentTransport(child, connection),
@@ -0,0 +1,43 @@
1
+ /**
2
+ * 「这条会话的 agent 自己说它在忙吗」——一个**弱**判据,不是归一化器。
3
+ *
4
+ * 真正的 vendor 原文 → 规范三态只发生在服务端 `axom-core` 的 `agentActivityOf`
5
+ * (前序 spec 裁决 L1:归一化只在一处)。这里要回答的问题窄得多,答案只有两种,
6
+ * 而且两边对「读不懂」的处置是同一条推理:服务端答 `null`(保留上一个已知状态,不写库),
7
+ * 这里答 `false`(本来源不主张忙碌,判据退回 `liveness.turnRunning`)。两边都不把未知
8
+ * 伪装成已知——答 `true` 会造出一个永不消失的假忙碌,正是本轮要消灭的东西。
9
+ *
10
+ * 两个键分属两套词汇表(claude 的 `state`、codex 的 `type`),**不做或运算**:
11
+ * `state` 在就只看 `state`。混读等于承认一份原文可以同时是两种方言。
12
+ */
13
+ export function activityIsBusy(raw) {
14
+ if (!raw)
15
+ return false;
16
+ if (typeof raw.state === 'string')
17
+ return raw.state === 'running' || raw.state === 'requires_action';
18
+ if (typeof raw.type === 'string')
19
+ return raw.type === 'active' && !codexBlockedOnHumanOnly(raw);
20
+ return false;
21
+ }
22
+ /**
23
+ * codex 的 `activeFlags` 里表示「agent 正被人挡住、没有在算」的标记。
24
+ *
25
+ * **和会话状态问的不是同一个问题。** 服务端的 `agentActivityOf` 要答「vendor 说它在干什么」,
26
+ * 这里要答「排干时有没有在途工作会被重启破坏」。一条停在审批卡或提问上的会话,两个答案不同:
27
+ * 它确实处在 vendor 说的 `active`,但**没有任何在途工作**,重启它不损失什么。把这种会话算成忙,
28
+ * 会让一次优雅更新白等满 30 分钟排干再报 BUSY —— 正是本分支要消灭的那个形状,只是换了个入口。
29
+ * 2026-09-21 在真机上抓到的第一条 codex 原文就是这一种:
30
+ * `{"type":"active","activeFlags":["waitingOnUserInput"]}`。
31
+ *
32
+ * 值域的取证、证据等级,以及「不认识的标记为什么保持忙」的理由,见 `axom-core` 的
33
+ * `AgentActivityMapping.kt` 里 `CODEX_BLOCKED_ON_HUMAN_FLAGS` 的 KDoc —— 那里是权威说明,
34
+ * 这两处必须同时改,所以判据故意写成同一个形状(非空 + 每一项都认得)。
35
+ */
36
+ const CODEX_BLOCKED_ON_HUMAN_FLAGS = new Set(['waitingOnApproval', 'waitingOnUserInput']);
37
+ /** `activeFlags` 非空**且每一项**都在 {@link CODEX_BLOCKED_ON_HUMAN_FLAGS} 里。 */
38
+ function codexBlockedOnHumanOnly(raw) {
39
+ const flags = raw.activeFlags;
40
+ if (!Array.isArray(flags) || flags.length === 0)
41
+ return false;
42
+ return flags.every((flag) => typeof flag === 'string' && CODEX_BLOCKED_ON_HUMAN_FLAGS.has(flag));
43
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * 两条厂商特定的活动状态采集路径。**只做搬运,不做归一化**——vendor→规范三态的映射
3
+ * 只发生在服务端 ingest 一处(裁决 L1:normalise in one place, server ingest)。
4
+ *
5
+ * 判 agent 用 `InitializeResponse.agentInfo.name`:ACP 的 `Implementation.name` 文档自述
6
+ * "intended for programmatic or logical use",而且用户改不了它(`agentName` 是工作站配置里的
7
+ * 条目名,随时可以改)。
8
+ */
9
+ const CLAUDE_AGENT = '@agentclientprotocol/claude-agent-acp';
10
+ const CODEX_AGENT = '@agentclientprotocol/codex-acp';
11
+ /** 这个 agent 有没有配套的活动状态采集器;按 InitializeResponse.agentInfo.name 判。 */
12
+ export function reportsActivity(agentName) {
13
+ return agentName === CLAUDE_AGENT || agentName === CODEX_AGENT;
14
+ }
15
+ /** Certified AIR task-stop compatibility, independent of activity collection. These adapters
16
+ * implement `_session/async_task/stop`; this is NOT a native child-session cancel declaration.
17
+ * Keep this separate even while the certified provider sets happen to coincide.
18
+ */
19
+ export function supportsAsyncTaskStop(agentName) {
20
+ return agentName === CLAUDE_AGENT || agentName === CODEX_AGENT;
21
+ }
22
+ /**
23
+ * claude-agent-acp 的 `emitRawSDKMessages`(`acp-agent.d.ts:645`,0.75.1 起存在):
24
+ * 命中过滤器的原始 SDK 消息会以 `extNotification("_claude/sdkMessage", …)` 推回来。
25
+ * 只要状态那一种——过滤器是 `SDKMessageFilter { type, subtype? }`(`:609`)。
26
+ */
27
+ export function newSessionMetaFor(agentName) {
28
+ if (agentName !== CLAUDE_AGENT)
29
+ return undefined;
30
+ return { claudeCode: { emitRawSDKMessages: [{ type: 'system', subtype: 'session_state_changed' }] } };
31
+ }
32
+ function asObject(value) {
33
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
34
+ ? value
35
+ : null;
36
+ }
37
+ /** claude:把 _claude/sdkMessage 的 params 提成 vendor 原文,非状态消息返回 null。 */
38
+ export function claudeActivityRaw(params) {
39
+ const message = asObject(params?.message);
40
+ if (!message)
41
+ return null;
42
+ if (message.type !== 'system' || message.subtype !== 'session_state_changed')
43
+ return null;
44
+ // 整条消息原样带走,不挑字段:服务端要的是证据,而将来 SDK 给这条消息加字段时
45
+ // 这里不该是需要改的地方。
46
+ return message;
47
+ }
48
+ /** codex:把 session_info_update 的 _meta 提成 vendor 原文,没有状态返回 null。 */
49
+ export function codexActivityRaw(meta) {
50
+ const codex = asObject(meta?.codex);
51
+ if (!codex)
52
+ return null;
53
+ return asObject(codex.threadStatus);
54
+ }