@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
@@ -1,90 +1,16 @@
1
1
  /**
2
- * "Adoption" — a user picking up a native agent session that already exists on this machine and
3
- * having its history replayed into Axom's timeline. Two ACP methods, both **optional on the
4
- * agent** and gated by `AcpConnection.capabilities` (Task 3): {@link listNativeSessions} calls
5
- * `session/list`, gated on `capabilities.list`; {@link replayNativeSession} calls `session/load`,
6
- * gated on `capabilities.loadSession`. Both degrade to "nothing to show" rather than an error when
7
- * unsupported — see the doc comment on each for why, and the deleted `bridges/types.ts`'s
8
- * `AgentBridge.listNativeSessions`/`BridgeSession.backfill` KDoc for the contract this carries
9
- * forward verbatim ("the directory hides the grouping, not an error").
10
- *
11
- * ## `session/resume` is deliberately NOT used here — read this before "fixing" it back
12
- *
13
- * The plan text that spawned this task says "`session/resume(replayFrom)`". Both do not exist in
14
- * `@agentclientprotocol/sdk@1.4.0`'s stable v1 entry point: `ResumeSessionRequest` has no
15
- * `replayFrom` field (that name only appears under `@agentclientprotocol/sdk/experimental/v2`,
16
- * which Global Constraint 1 forbids importing), and — more fundamentally — `resumeSession`'s own
17
- * doc comment in `dist/acp.d.ts` says it "Resumes an existing session **without** replaying the
18
- * message history (**unlike** `session/load`)". `session/load` (`loadSession`, gated by the
19
- * top-level `AgentCapabilities.loadSession: boolean`, not `sessionCapabilities`) is the ACP v1
20
- * method whose own doc comment says the agent should "Stream the entire conversation history back
21
- * to the client via notifications" — i.e. it is the actual replay mechanism this file needs.
22
- * `session/resume` exists for a client that already has a session's history cached elsewhere and
23
- * wants to keep working without paying for a redundant replay; Axom has never seen an adopted
24
- * session's history before, so that case never applies here.
25
- *
26
- * Verified both target adapters actually support the method this file calls: `claude-agent-acp`
27
- * 0.70.0 and `codex-acp` 1.6.2 both declare `agentCapabilities.loadSession: true` and both
28
- * implement `session/load` (`.superpowers/research/acp-sdk-registry.md`, "VERIFIED" section).
29
- *
30
- * ## Observing the replayed `session/update` notifications
31
- *
32
- * Unlike `session/new` (Task 4's `AcpSession`, built on the SDK's `ClientContext.buildSession(...)`
33
- * convenience wrapper), the SDK has no `SessionBuilder`-equivalent helper for `session/load` — the
34
- * only public, supported way to see raw `session/update` notifications outside of that helper is a
35
- * `ClientApp`-level `onNotification(methods.client.session.update, ...)` handler (the pattern the
36
- * SDK's own `acp.test.js` uses throughout for exactly this). That has to be registered before
37
- * `ClientApp.connect()` copies its handler table (`acpClient.ts`'s `ConnectAgentOptions.configure`
38
- * doc comment; `permission.ts`'s file doc comment says the same for `session/request_permission`).
39
- * {@link createReplayCapture}/{@link ReplayCapture.register} exist for that: a caller passes
40
- * `capture.register` into `connectAgent`'s `configure` hook — composable with `permission.ts`'s
41
- * `registerPermissionHandlers` in the same hook, since `configure` is one function a caller can do
42
- * several registrations inside of — and later, once the connection exists, passes the *same*
43
- * `capture` into {@link replayNativeSession}. The registered handler sees *every* `session/update`
44
- * notification on the connection, not just the one being replayed (verified against
45
- * `SessionUpdateRouter.handleMessage`, which always returns `Handled.no(message)` — it never consumes
46
- * the message, so dispatch keeps walking the handler chain to whatever else is registered, including
47
- * the SDK's own internal `session/new`-scoped router for a live `AcpSession` on the *same* connection).
48
- * That is exactly the axis review found this file getting wrong in fix round 1: a stray comment here
49
- * used to say this "never interferes with a live `AcpSession` on some other connection" — true, but
50
- * irrelevant, since the real conflict is a live `AcpSession` on the *same* connection (the shape a
51
- * caller composing this with `registerPermissionHandlers` in one `configure` hook produces routinely).
52
- * Without gating, the handler above would buffer *every* session's updates forever — only
53
- * {@link ReplayCapture.take} ever releases a buffered entry, and it is only ever called for the one
54
- * `sessionId` being replayed, so any other session's notifications arriving on this connection (most
55
- * concretely: a live `AcpSession` opened on the same connection right after adoption, which Task 8
56
- * needs) would accumulate unbounded for the life of the connection. {@link ReplayCapture.arm} exists
57
- * to close that: the handler now only buffers a `sessionId` that has been armed, {@link
58
- * replayNativeSession} arms its target *before* sending `session/load`, and `take()` disarms on its
59
- * way out — so the buffer's upper bound is whatever is currently being replayed, never every session
60
- * this connection has ever seen an update for.
61
- *
62
- * ## Replay goes through the same mapping and folding the live path uses — reimplemented, not shared
63
- *
64
- * {@link replayNativeSession} maps every replayed `SessionUpdate` through the exact same
65
- * `mapSessionUpdate` (`sessionUpdateMapper.ts`) the live path (`AcpSession`) uses, and every mapped
66
- * event through the same `validateWireEvent` (`validate.ts`) outbound check — so it can never
67
- * emit anything the server can't decode, and (per `mapSessionUpdate`'s own contract) can never emit
68
- * `final_result`. What it does NOT reuse is `AcpSession`'s own `agent_message` synthesis /
69
- * synthesis: that lives in `AcpSession` as push-based, event-sink-driven instance state (see its
70
- * class doc comment), a different execution shape than this file's pull-based `AsyncGenerator` over
71
- * an already-complete update list. Task 4's report flagged this by name: "the replay path needs its
72
- * own equivalent folding and message-completion logic — do not assume the live path's state machine
73
- * is doing it for you." {@link foldReplayEvents} is that equivalent, reimplemented as a single pass
74
- * over a finished array instead of instance fields mutated across live callbacks.
75
- *
76
- * **The `user_message` half of that duplication is gone.** This file used to carry its own copy of
77
- * `AcpSession.foldUserMessageChunk`, and the two had already drifted: replay flushed a buffered user
78
- * message on *any* intervening event, the live path only on a different `messageId` or turn end, so
79
- * the same transcript produced a different number of cards depending on which path replayed it.
80
- * Phase 7 moved the fold to `CoworkEngine`, which already buffered `agent_message_chunk` by exactly
81
- * the ACP rule that covers all three chunk kinds (`ContentChunk`, `types.gen.d.ts:3456`).
82
- * `user_message_chunk` now passes through here untouched, and `ExternalAgentEventApplier`'s
83
- * `onBackfillDone` is what closes the last buffered message.
2
+ * "Adoption" — the native agent sessions that already exist on this machine, as an agent lists them, and deleting them.
3
+ * Short-lived connections only (`connectAgent`); both ACP methods are **optional on the agent** and gated by
4
+ * `AcpConnection.capabilities`: {@link listNativeSessions} calls `session/list` (gated on `capabilities.list`),
5
+ * {@link deleteNativeSessions} calls `session/delete` (gated on `capabilities.delete`). Both degrade to "nothing to show" /
6
+ * `unsupported` rather than an error when the method is not advertised — carried forward verbatim from the deleted
7
+ * `bridges/types.ts`'s `AgentBridge.listNativeSessions` contract ("the directory hides the grouping, not an error").
8
+ *
9
+ * Replaying an adopted session's history (`session/load`) is the processor's (`processor/updates.ts`, the history window
10
+ * the daemon opens with `agent_session_open{mode:'load'}`); the replay helpers this file used to carry were deleted with the
11
+ * old live path (Task 29).
84
12
  */
85
13
  import { methods } from '@agentclientprotocol/sdk';
86
- import { validateWireEvent } from './validate.js';
87
- import { mapSessionUpdate, SessionMapperState } from './sessionUpdateMapper.js';
88
14
  /** `NativeSessionInfo.title` is truncated to this many characters — mirrors the deleted `claudeCode/adoption.ts`'s display-safety guard (`TITLE_MAX_LENGTH`). */
89
15
  const TITLE_MAX_LENGTH = 80;
90
16
  /** `SessionInfo.updatedAt` is an ISO 8601 string or `null`/absent; missing or unparseable sorts as oldest (0) rather than throwing. */
@@ -94,6 +20,24 @@ function parseUpdatedAt(value) {
94
20
  const parsed = Date.parse(value);
95
21
  return Number.isNaN(parsed) ? 0 : parsed;
96
22
  }
23
+ /**
24
+ * `session/delete` per id. Only when the agent advertised `sessionCapabilities.delete` — ACP forbids calling an unadvertised method. Codex's delete is an archive (`thread/archive`); that is still "gone from session/list", which is all the caller needs.
25
+ */
26
+ export async function deleteNativeSessions(connection, ids) {
27
+ if (!connection.capabilities.delete)
28
+ return ids.map((nativeSessionId) => ({ nativeSessionId, outcome: 'unsupported' }));
29
+ const results = [];
30
+ for (const nativeSessionId of ids) {
31
+ try {
32
+ await connection.connection.agent.request(methods.agent.session.delete, { sessionId: nativeSessionId });
33
+ results.push({ nativeSessionId, outcome: 'deleted' });
34
+ }
35
+ catch (error) {
36
+ results.push({ nativeSessionId, outcome: 'failed', error: error instanceof Error ? error.message : String(error) });
37
+ }
38
+ }
39
+ return results;
40
+ }
97
41
  /**
98
42
  * List an agent's native sessions scoped to `query.cwd`, newest first.
99
43
  *
@@ -122,275 +66,3 @@ export async function listNativeSessions(connection, query) {
122
66
  .sort((a, b) => b.updatedAtEpochMs - a.updatedAtEpochMs);
123
67
  return response.nextCursor != null ? { sessions, nextCursor: response.nextCursor } : { sessions };
124
68
  }
125
- export function createReplayCapture() {
126
- const armed = new Set();
127
- const buffered = new Map();
128
- const listeners = new Map();
129
- return {
130
- register(app) {
131
- app.onNotification(methods.client.session.update, (ctx) => {
132
- // Armed wins over listening: while a session/load replay is in flight, its history must land
133
- // in the buffer rather than in a live session's read loop — see `listen`'s doc comment.
134
- if (armed.has(ctx.params.sessionId)) {
135
- const list = buffered.get(ctx.params.sessionId) ?? [];
136
- list.push(ctx.params.update);
137
- buffered.set(ctx.params.sessionId, list);
138
- return;
139
- }
140
- // Neither armed nor listened to: dropped, so this handler cannot accumulate every session
141
- // the connection has ever seen an update for (see the file doc comment).
142
- listeners.get(ctx.params.sessionId)?.(ctx.params.update);
143
- });
144
- },
145
- arm(sessionId) {
146
- armed.add(sessionId);
147
- },
148
- take(sessionId) {
149
- armed.delete(sessionId);
150
- const list = buffered.get(sessionId) ?? [];
151
- buffered.delete(sessionId);
152
- return list;
153
- },
154
- count(sessionId) {
155
- return buffered.get(sessionId)?.length ?? 0;
156
- },
157
- listen(sessionId, sink) {
158
- listeners.set(sessionId, sink);
159
- return () => {
160
- // Only clear our own entry: a later listen() for the same sessionId may already have
161
- // replaced it, and unsubscribing then would silence the session that is actually running.
162
- if (listeners.get(sessionId) === sink)
163
- listeners.delete(sessionId);
164
- };
165
- },
166
- };
167
- }
168
- /** `payload[key]` narrowed to `string`, or `null` — same helper `acpSession.ts` uses for the identical reason (`payload` is `Record<string, unknown>` by construction). */
169
- function stringField(payload, key) {
170
- const value = payload[key];
171
- return typeof value === 'string' ? value : null;
172
- }
173
- /**
174
- * Map a completed list of replayed `SessionUpdate`s to wire events, applying the same
175
- * message-completion synthesis {@link AcpSession} applies to the live stream — see the file doc
176
- * comment for why this is a reimplementation, not a shared call, and `acpSession.ts`'s
177
- * `trackMessageBoundary`/`flushMessageCompleted` for the rule being mirrored:
178
- *
179
- * - `agent_message` is synthesized whenever the streaming `agent_message_chunk`/`agent_thought_chunk`
180
- * `messageId` changes, and once more at the very end of the list (there is no live "turn end"
181
- * signal to hang this off during replay — a whole adopted session's history has no `PromptResponse`
182
- * in it at all — so "end of the replayed list" is this function's equivalent of `AcpSession.finishTurn`'s
183
- * unconditional turn-end flush).
184
- * - A `user_message_chunk` still forces that synthesis: a user turn beginning means whatever the
185
- * assistant was streaming has necessarily already ended. The chunk itself is emitted verbatim —
186
- * folding it is the server's job now (see the file doc comment).
187
- *
188
- * Exported directly (not just through {@link replayNativeSession}) so its synthesis behavior can be
189
- * pinned by unit tests with no process spawning involved — the process-spawning tests are reserved
190
- * for what only a real subprocess can prove (capability gating, the actual RPC shape, ordering
191
- * against the real `session/load` response).
192
- */
193
- export function foldReplayEvents(updates) {
194
- const events = [];
195
- // Fresh per replay, not shared with the live session that may follow: this call reconstructs one
196
- // adopted session's history from its own `tool_call`s, and a registry carried in from elsewhere
197
- // could name a call this history never created.
198
- const mapperState = new SessionMapperState();
199
- let sawMessageContent = false;
200
- let currentMessageId = null;
201
- function flushMessageCompleted() {
202
- if (!sawMessageContent)
203
- return;
204
- events.push({ eventName: 'agent_message', payload: { messageId: currentMessageId ?? 'replay' } });
205
- sawMessageContent = false;
206
- currentMessageId = null;
207
- }
208
- for (const update of updates) {
209
- for (const event of mapSessionUpdate(update, mapperState)) {
210
- if (event.eventName === 'user_message_chunk') {
211
- // A user turn beginning means whatever the assistant was streaming has necessarily already
212
- // ended — flush it first, mirroring AcpSession.finishTurn's ordering at a live turn
213
- // boundary (just the mirror image here, since replay sees the *next* user turn rather than
214
- // an explicit turn-end signal). The chunk itself then goes through verbatim: the fold that
215
- // used to live here is the server's now.
216
- flushMessageCompleted();
217
- events.push(event);
218
- continue;
219
- }
220
- if (event.eventName === 'agent_message_chunk' || event.eventName === 'agent_thought_chunk') {
221
- const messageId = stringField(event.payload, 'messageId');
222
- if (sawMessageContent && messageId !== null && currentMessageId !== null && messageId !== currentMessageId) {
223
- flushMessageCompleted();
224
- }
225
- sawMessageContent = true;
226
- if (messageId !== null)
227
- currentMessageId = messageId;
228
- }
229
- events.push(event);
230
- }
231
- }
232
- flushMessageCompleted();
233
- return events;
234
- }
235
- /**
236
- * Wait until the `session/update` notifications an agent sent *before* answering a request have
237
- * actually been **dispatched**, which is **not** implied by that request having responded.
238
- *
239
- * The SDK handles the two message kinds on different schedules (`jsonrpc.js`'s
240
- * `receiveWireMessage`): a response is applied **synchronously** via `handleResponse` the moment the
241
- * reader pulls it off the stream, while a notification is handed to `processIncomingMessage`, which
242
- * is *not* awaited by the read loop and which awaits each registered handler in turn. So even though
243
- * the agent writes every `session/update` before its response — and the reader therefore reads them
244
- * first — the response's `await` can resume before a single one of our handlers has run. Measured
245
- * directly on both request paths that have this shape:
246
- *
247
- * - `session/load` ({@link replayNativeSession}): the order observed was `response`, then the
248
- * updates, so `take()` on the spot returned a partial history, or none at all.
249
- * - `session/prompt` (`acpSession.ts`'s `AcpSession.attach`): the turn's terminal `stop` message was
250
- * enqueued **ahead of** the tail of the same turn's deltas — `agent_message` and the
251
- * `TurnResult` landing mid-stream, with the remaining text arriving as orphans after the item the
252
- * server had already closed.
253
- *
254
- * The difference between "works" and "reorders" is nothing but how many handlers happen to be
255
- * registered on the connection — which is why the same code looked correct until the manager wired
256
- * permission handlers onto the same `ClientApp`.
257
- *
258
- * The one guarantee that survives is ordering *into* the dispatcher: by the time the response is
259
- * applied, every preceding notification's dispatch has been **started**. This waits for a macrotask
260
- * boundary, which drains a chain of synchronous handlers' microtask continuations, and keeps waiting
261
- * while `count()` is still growing.
262
- *
263
- * ## This is a heuristic. It is bounded **and** it truncates — three ways, all measured
264
- *
265
- * Nothing enforces the premise it rests on, so read these as live failure modes rather than as
266
- * theoretical corners:
267
- *
268
- * | Condition | Measured outcome |
269
- * |---|---|
270
- * | Any handler on the chain awaits a **macrotask** (even `setTimeout(…, 0)`) | 6 updates → **0** replayed |
271
- * | The agent answers **before** it has finished streaming | 6 updates → **4** replayed |
272
- * | The stream outlasts `maxRounds` | 3000 updates → **2086** replayed |
273
- *
274
- * The first is why "every handler on this chain is synchronous" is a **prerequisite**, and why it is
275
- * written down at the place that actually establishes it — `acpAgents.ts`'s `AcpAgents.open`
276
- * `configure` hook, the one site where handlers get registered — rather than only here. A function
277
- * cannot enforce a property of a registration it never sees.
278
- *
279
- * The second is outside this function's reach entirely: an agent that responds mid-stream has broken
280
- * the ordering guarantee this is built on, and no amount of waiting recovers it.
281
- *
282
- * The third is `maxRounds`, the backstop against an agent that keeps streaming forever — a wait with
283
- * no exit would hang the whole open. It is reachable in practice, so hitting it is **logged** rather
284
- * than returned quietly: that diagnostic is the whole line between "bounded degradation" and
285
- * "invisible data loss", and it is the only one of the three this function can see happening to
286
- * itself.
287
- *
288
- * `count` is supplied by the caller rather than read off a `ReplayCapture` because the two callers
289
- * count different things: the replay path counts buffered updates for one armed `sessionId`, the
290
- * live path counts updates delivered into an attached session's queue.
291
- */
292
- export async function settleUpdates(sessionId, count, maxRounds = 10) {
293
- let previous = -1;
294
- for (let round = 0; round < maxRounds; round += 1) {
295
- const before = count();
296
- // setImmediate, not setTimeout(0): it runs in the check phase, after the poll phase has drained
297
- // whatever the transport already had readable, so one turn covers both the microtasks and any
298
- // further messages sitting in the socket buffer.
299
- await new Promise((resolve) => setImmediate(resolve));
300
- const after = count();
301
- if (after === before && after === previous)
302
- return;
303
- previous = before;
304
- }
305
- // Falling out of the loop is not "settled" — it is "gave up while the agent was still streaming".
306
- // Whatever arrives from here on may be reordered against the turn/replay boundary this call was
307
- // holding, or dropped outright. Never silent: a session that quietly loses the tail of its history
308
- // is indistinguishable from an agent that had no history to give.
309
- console.error(`[acp] session/update stream for '${sessionId}' was still growing after ${maxRounds} settle rounds ` +
310
- `(${count()} updates so far) — continuing anyway; later updates may be reordered or lost.`);
311
- }
312
- /**
313
- * Replay one native session's history as wire events via `session/load` — see the file doc comment
314
- * for why `session/load`, not `session/resume`.
315
- *
316
- * When the agent doesn't advertise `agentCapabilities.loadSession` (`connection.capabilities.loadSession`),
317
- * this yields nothing **without ever calling `session/load`** — same "hidden grouping, not an error"
318
- * contract as {@link listNativeSessions}, applied to the capability that actually gates replay.
319
- *
320
- * Every yielded event has already gone through {@link foldReplayEvents} (same mapping the live path
321
- * uses, plus the same `agent_message` synthesis) and `validateWireEvent` (same outbound check the live path's
322
- * `permission_request` events go through) — an event that fails validation is dropped with a
323
- * diagnostic, never thrown, matching `validate.ts`'s own "drop + diagnostic, never an
324
- * exception" design. `mapSessionUpdate` never produces `final_result` (Task 4's contract, unchanged
325
- * here since this function adds no new event-producing path of its own) and `foldReplayEvents` adds
326
- * no new event names beyond what `mapSessionUpdate` and this file's own `agent_message` synthesis
327
- * already produce — both already appear in the server's understood vocabulary
328
- * (`sessionUpdateMapper.test.ts`'s `SERVER_UNDERSTOOD_EVENT_NAMES`), so nothing here needs to extend
329
- * it.
330
- *
331
- * ## `session/load` also activates the session, not just replays it — and Task 8 relies on that
332
- *
333
- * The `session/load` request below sends `mcpServers: []`. That is not incidental — `session/load`
334
- * is not a read-only "give me the history" call: per its own spec, the agent restores this
335
- * `sessionId` as its **current** session, the same way `session/new` would, just with history
336
- * attached instead of starting empty.
337
- *
338
- * Task 8 resolved the fork this once handed forward: **Axom keeps prompting on
339
- * `params.nativeSessionId`, the session `session/load` just activated**, rather than opening a fresh
340
- * `session/new` and treating the replay as read-only history. `agentSessions.ts` receives a
341
- * `nativeSessionId` on reconnect as well as on adoption (the server persists it and hands it back),
342
- * so "open a new session and replay read-only" would silently drop the agent's own context on every
343
- * single reconnect — a regression against the deleted Claude Code bridge, which really did resume.
344
- * `AcpSession.attach` (`acpSession.ts`) is what that costs: the SDK's `ActiveSession` has a private
345
- * constructor reachable only through `SessionBuilder.start()`, i.e. only through `session/new`, so
346
- * driving an already-loaded `sessionId` means hand-building the same small `DrivenSession` surface
347
- * around raw `session/prompt` / `session/cancel` requests and {@link ReplayCapture.listen}.
348
- *
349
- * `mcpServers: []` is consistent with the other path rather than a shortfall of this one:
350
- * `AcpSession.open`'s `buildSession(cwd).start()` attaches no MCP servers either (`OpenSessionOptions`
351
- * carries only `cwd`), because Axom does not configure MCP servers for external agents at all.
352
- */
353
- export async function* replayNativeSession(connection, capture, params) {
354
- if (!connection.capabilities.loadSession)
355
- return;
356
- // Arm before requesting — the agent may start sending session/update notifications for this
357
- // sessionId as soon as it receives the request, and the capture's handler drops anything for a
358
- // sessionId that isn't armed yet (see ReplayCapture.arm's doc comment).
359
- capture.arm(params.nativeSessionId);
360
- // The transport diverts updates for a session it does not know is handled here, because the SDK's
361
- // router would otherwise drop them on the floor. During a replay this capture *is* the handler,
362
- // so the claim has to be made explicitly — and withdrawn in the same `finally` that disarms.
363
- const unmarkSession = connection.markSessionKnown(params.nativeSessionId);
364
- let updates;
365
- try {
366
- const response = await connection.connection.agent.request(methods.agent.session.load, {
367
- sessionId: params.nativeSessionId,
368
- cwd: params.cwd,
369
- mcpServers: [],
370
- });
371
- params.onLoaded?.(response);
372
- // The response resolving is NOT the end of the replay — see {@link settleUpdates} for the SDK
373
- // scheduling that makes `take()` on the spot return a partial history.
374
- await settleUpdates(params.nativeSessionId, () => capture.count(params.nativeSessionId));
375
- }
376
- finally {
377
- unmarkSession();
378
- // In a `finally` because `arm()` has no other exit. A rejected `session/load` — a stale id the
379
- // agent has since deleted, the case `AcpAgents.startSession` catches and falls back from — used
380
- // to throw straight past `take()`, leaving the id armed and whatever had already been buffered
381
- // for it held until the connection died. That contradicts this file's own claim that "the
382
- // buffer's upper bound is whatever is currently being replayed": the fallback opens a *different*
383
- // session id, so nothing would ever come back to take this one. `take()` is both halves at once
384
- // (release the buffer, disarm), which is why the failure path calls the same function the
385
- // success path does rather than a disarm of its own.
386
- updates = capture.take(params.nativeSessionId);
387
- }
388
- for (const event of foldReplayEvents(updates)) {
389
- const rejection = validateWireEvent(event);
390
- if (rejection) {
391
- console.error(`[acp] dropping a replayed event that fails validateWireEvent: ${rejection.reason} (session ${params.nativeSessionId})`);
392
- continue;
393
- }
394
- yield event;
395
- }
396
- }
@@ -50,3 +50,39 @@ export function sniffAuthUrl(line) {
50
50
  return undefined;
51
51
  return match[0].replace(TRAILING_DECORATION, '');
52
52
  }
53
+ /** How many lines after an invitation a URL may still arrive on. Two covers the measured shape with one spare. */
54
+ const INVITATION_REACH = 2;
55
+ /**
56
+ * {@link sniffAuthUrl} with one line of memory, for a login process's own output.
57
+ *
58
+ * A device-code login measured on a real machine prints the invitation and the URL on **adjacent**
59
+ * lines (`Please visit the following URL…` / `Verification URL: https://…`), which the single-line
60
+ * rule misses entirely. This remembers an invitation for {@link INVITATION_REACH} lines.
61
+ *
62
+ * Deliberately **not** used by the stderr sniffer on login connections: that one covers an `npx`
63
+ * install's output too, where widening the match would pick up documentation links. This one is only
64
+ * ever pointed at a process whose whole job is to sign in.
65
+ */
66
+ export function createAuthUrlScanner() {
67
+ let reach = 0;
68
+ return (line) => {
69
+ const direct = sniffAuthUrl(line);
70
+ if (direct) {
71
+ reach = 0;
72
+ return direct;
73
+ }
74
+ const clean = stripAnsi(line);
75
+ if (INVITATION_WORDS.test(clean)) {
76
+ reach = INVITATION_REACH;
77
+ return undefined;
78
+ }
79
+ if (reach === 0)
80
+ return undefined;
81
+ reach -= 1;
82
+ const match = ABSOLUTE_URL.exec(clean);
83
+ if (!match)
84
+ return undefined;
85
+ reach = 0;
86
+ return match[0].replace(TRAILING_DECORATION, '');
87
+ };
88
+ }
@@ -29,13 +29,14 @@
29
29
  * but the result says so via `checksumVerified: false`. Nothing here may report an unverified
30
30
  * download as checked; that is precisely the distinction {@link InstallOutcome} exists to preserve.
31
31
  *
32
- * ## Layout: several versions can coexist
32
+ * ## Layout: several versions can coexist, and each appears whole or not at all
33
33
  *
34
- * Installs land at `<root>/<registryAgent>/<version>/`, one directory per version, the same
35
- * side-by-side layout npm's own `_npx` cache keeps for npx-distributed adapters. Picking one
36
- * installed version when several are present reuses `installProbe.ts`'s
37
- * {@link pickInstalledVersion} rather than reimplementing that rule a second time — see
38
- * {@link installedBinaryVersion}.
34
+ * Installs land at `<root>/bin/<registryAgent>/<version>/` (spec 2026-10-11 §6.2), one directory per
35
+ * version. The archive is unpacked into `<root>/.staging/<uuid>/` and only a complete tree is
36
+ * `rename(2)`d into place, so a directory under `bin/` always holds a whole install — an unpack that
37
+ * is killed half-way leaves only a staging leftover, never a half-written version that something
38
+ * would take for installed. Which version an agent *launches* is no longer chosen here: it is the
39
+ * agent's `current` build (`builds/store.ts`). {@link installedBinaryVersion} only enumerates.
39
40
  *
40
41
  * ## Unpacking without a new dependency
41
42
  *
@@ -65,12 +66,14 @@
65
66
  * `<installDir>/<basename of the archive URL>` and marked executable — that filename is exactly
66
67
  * what `BinaryTarget.cmd` refers to. See {@link isArchiveFile}.
67
68
  */
68
- import { createHash } from 'node:crypto';
69
+ import { createHash, randomUUID } from 'node:crypto';
69
70
  import { createWriteStream, promises as fs, readdirSync } from 'node:fs';
70
71
  import { tmpdir } from 'node:os';
71
72
  import path from 'node:path';
72
73
  import { Readable } from 'node:stream';
73
74
  import { pipeline } from 'node:stream/promises';
75
+ import { binaryBuildDir, inside } from './builds/model.js';
76
+ import { publishStaged } from './builds/store.js';
74
77
  import { pickInstalledVersion } from './installProbe.js';
75
78
  import { runTimedCommand } from './timedCommand.js';
76
79
  const PLATFORM_SEGMENT = {
@@ -122,12 +125,13 @@ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
122
125
  export function installedBinaryVersion(registryAgent, root, pinned) {
123
126
  let entries;
124
127
  try {
125
- entries = readdirSync(path.join(root, registryAgent), { withFileTypes: true });
128
+ entries = readdirSync(path.join(root, 'bin', registryAgent), { withFileTypes: true });
126
129
  }
127
130
  catch {
128
131
  return undefined;
129
132
  }
130
- const versions = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
133
+ // Dot-directories (`.staging`, `.trash`, editor droppings) are never versions.
134
+ const versions = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.')).map((entry) => entry.name);
131
135
  return pickInstalledVersion(versions, pinned);
132
136
  }
133
137
  /**
@@ -203,9 +207,40 @@ const ARCHIVE_DOWNLOAD_TIMEOUT_MS = 300_000;
203
207
  * longer than the download step right above it is already allowed to take.
204
208
  */
205
209
  const ARCHIVE_EXTRACT_TIMEOUT_MS = 300_000;
210
+ /**
211
+ * Attempts made on the *request* phase of one archive download. Registry archives are
212
+ * third-party-hosted, and the failure this retries — the initial `fetch` throwing — is the
213
+ * transient kind: an edge node that answers this minute and not the next, a resolver rotating
214
+ * between a real and a dead address. Deterministic failures (redirect policy, loops, HTTP codes)
215
+ * come back from {@link fetchArchive} as structured results and are never retried. Both attempts
216
+ * share one {@link ARCHIVE_DOWNLOAD_TIMEOUT_MS} signal, so the whole-request budget never doubles;
217
+ * undici's own ~10s connect bound keeps the added worst case small.
218
+ */
219
+ const ARCHIVE_FETCH_ATTEMPTS = 2;
220
+ /** Pause between the two fetch attempts — long enough for a flipped resolver answer, short next to a 300s budget. */
221
+ const ARCHIVE_FETCH_RETRY_PAUSE_MS = 2_000;
206
222
  function errorMessage(err) {
207
223
  return err instanceof Error ? err.message : String(err);
208
224
  }
225
+ /**
226
+ * `fetch`'s failures arrive as a bare `fetch failed` with the real reason (a connect timeout, a
227
+ * reset mid-handshake, a TLS refusal — classes that lead the diagnosis, not decorate it) one or
228
+ * two `cause` links down, and undici names them with stable machine codes (`UND_ERR_CONNECT_TIMEOUT`,
229
+ * `ECONNRESET`). This walks the chain and joins it onto the message, so a daemon log and an error
230
+ * card say what actually happened instead of leaving a one-word verdict. Observed 2026-10-14: a
231
+ * machine whose route to `dl.google.com` had died logged only "fetch failed" and cost a DNS/CDN
232
+ * investigation that `UND_ERR_CONNECT_TIMEOUT` would have named on the spot.
233
+ */
234
+ function describeFetchFailure(err) {
235
+ const parts = [errorMessage(err)];
236
+ let cause = err?.cause;
237
+ for (let depth = 0; depth < 5 && cause !== undefined && cause !== null; depth += 1) {
238
+ const named = cause;
239
+ parts.push(typeof named.code === 'string' ? named.code : typeof named.message === 'string' ? named.message : String(cause));
240
+ cause = cause.cause;
241
+ }
242
+ return [...new Set(parts)].join(' — ');
243
+ }
209
244
  function hasEntries(dir) {
210
245
  try {
211
246
  return readdirSync(dir).length > 0;
@@ -308,7 +343,7 @@ async function extractArchive(archivePath, destDir, exec, timeoutMs) {
308
343
  }
309
344
  /**
310
345
  * Fetch, verify and unpack `agent`'s binary adapter for this machine's platform triple. Never
311
- * starts the agent — the counterpart to `installNpxPackage`/`installUvTool`'s "download, don't run"
346
+ * starts the agent — the counterpart to `ensurePristine`/`installUvBuild`'s "download, don't run"
312
347
  * contract.
313
348
  *
314
349
  * ## The three outcomes the brief for this task requires, and why each looks the way it does
@@ -340,7 +375,13 @@ export async function installBinary(agent, opts) {
340
375
  return { ok: false, error: `Agent '${agent.id}' publishes no archive for ${triple ?? `${platform}-${arch}`}.` };
341
376
  }
342
377
  const version = agent.version;
343
- const installDir = path.join(opts.root, agent.id, version);
378
+ let installDir;
379
+ try {
380
+ installDir = inside(opts.root, binaryBuildDir(agent.id, version));
381
+ }
382
+ catch (err) {
383
+ return { ok: false, error: errorMessage(err) };
384
+ }
344
385
  if (hasEntries(installDir)) {
345
386
  return { ok: true, checksumVerified: false, alreadyInstalled: true, version, installDir };
346
387
  }
@@ -358,8 +399,24 @@ export async function installBinary(agent, opts) {
358
399
  // whereas a `BinaryTarget.archive` is an arbitrary third-party URL verified by an optional
359
400
  // SHA-256 the registry supplies. Different trust model, so the policy is stated here: one
360
401
  // whole-chain budget, a capped number of hops, and HTTPS at every one. See `fetchArchive` for
361
- // why following redirects is required rather than merely permitted.
362
- const fetched = await fetchArchive(target.archive, fetchImpl, AbortSignal.timeout(ARCHIVE_DOWNLOAD_TIMEOUT_MS));
402
+ // why following redirects is required rather than merely permitted. The one retry here covers
403
+ // only the transient request-phase throw ({@link ARCHIVE_FETCH_ATTEMPTS}).
404
+ const signal = AbortSignal.timeout(ARCHIVE_DOWNLOAD_TIMEOUT_MS);
405
+ let fetched;
406
+ for (let attempt = 1;; attempt += 1) {
407
+ try {
408
+ fetched = await fetchArchive(target.archive, fetchImpl, signal);
409
+ break;
410
+ }
411
+ catch (err) {
412
+ if (attempt >= ARCHIVE_FETCH_ATTEMPTS || signal.aborted) {
413
+ throw new Error(describeFetchFailure(err));
414
+ }
415
+ console.warn(`[acp] archive fetch attempt ${attempt} for '${agent.id}' failed ` +
416
+ `(${describeFetchFailure(err)}); retrying once.`);
417
+ await new Promise((resolve) => setTimeout(resolve, ARCHIVE_FETCH_RETRY_PAUSE_MS));
418
+ }
419
+ }
363
420
  if (!fetched.ok) {
364
421
  return { ok: false, error: `Failed to download adapter archive for '${agent.id}': ${fetched.error}` };
365
422
  }
@@ -374,7 +431,14 @@ export async function installBinary(agent, opts) {
374
431
  const hash = createHash('sha256');
375
432
  const source = Readable.fromWeb(response.body);
376
433
  source.on('data', (chunk) => hash.update(chunk));
377
- await pipeline(source, createWriteStream(archivePath, { mode: 0o600 }));
434
+ // A body that dies mid-stream (a reset connection, undici's "terminated") is a download
435
+ // failure like any other — reported, not thrown past the caller that owns the retry UX.
436
+ try {
437
+ await pipeline(source, createWriteStream(archivePath, { mode: 0o600 }));
438
+ }
439
+ catch (err) {
440
+ return { ok: false, error: `Failed to download adapter archive for '${agent.id}': ${describeFetchFailure(err)}` };
441
+ }
378
442
  if (target.sha256) {
379
443
  const digest = hash.digest('hex');
380
444
  if (digest.toLowerCase() !== target.sha256.toLowerCase()) {
@@ -385,23 +449,32 @@ export async function installBinary(agent, opts) {
385
449
  };
386
450
  }
387
451
  }
388
- await fs.mkdir(installDir, { recursive: true });
452
+ // Unpacked into a staging tree on the same filesystem, then renamed into place whole: nothing
453
+ // under `bin/` is ever half-written (see the module doc's layout section).
454
+ const staged = path.join(opts.root, '.staging', randomUUID());
389
455
  try {
390
- if (await isArchiveFile(archivePath, target.archive)) {
391
- await extractArchive(archivePath, installDir, opts.exec ?? runCommand, ARCHIVE_EXTRACT_TIMEOUT_MS);
456
+ await fs.mkdir(staged, { recursive: true });
457
+ try {
458
+ if (await isArchiveFile(archivePath, target.archive)) {
459
+ await extractArchive(archivePath, staged, opts.exec ?? runCommand, ARCHIVE_EXTRACT_TIMEOUT_MS);
460
+ }
461
+ else {
462
+ // Not an archive at all — the download *is* the program (e.g. `sigit`'s Linux/Windows
463
+ // targets, see the module doc). Place it under the same name `BinaryTarget.cmd` refers to
464
+ // and mark it executable; `chmod` is a no-op on Windows but harmless there.
465
+ const destPath = path.join(staged, archiveFileName(target.archive));
466
+ await fs.copyFile(archivePath, destPath);
467
+ await fs.chmod(destPath, 0o755);
468
+ }
392
469
  }
393
- else {
394
- // Not an archive at all — the download *is* the program (e.g. `sigit`'s Linux/Windows
395
- // targets, see the module doc). Place it under the same name `BinaryTarget.cmd` refers to
396
- // and mark it executable; `chmod` is a no-op on Windows but harmless there.
397
- const destPath = path.join(installDir, archiveFileName(target.archive));
398
- await fs.copyFile(archivePath, destPath);
399
- await fs.chmod(destPath, 0o755);
470
+ catch (err) {
471
+ return { ok: false, error: `Failed to unpack adapter archive for '${agent.id}': ${errorMessage(err)}` };
400
472
  }
473
+ // A concurrent install of the same version may have published first; its tree is equally whole.
474
+ await publishStaged(staged, installDir);
401
475
  }
402
- catch (err) {
403
- await fs.rm(installDir, { recursive: true, force: true }).catch(() => undefined);
404
- return { ok: false, error: `Failed to unpack adapter archive for '${agent.id}': ${errorMessage(err)}` };
476
+ finally {
477
+ await fs.rm(staged, { recursive: true, force: true }).catch(() => undefined);
405
478
  }
406
479
  return { ok: true, checksumVerified: Boolean(target.sha256), alreadyInstalled: false, version, installDir };
407
480
  }