@platless/axom-cli 0.3.13 → 0.6.36

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 (320) hide show
  1. package/README.md +18 -5
  2. package/dist/axom-patch/cli/README.md +119 -9
  3. package/dist/axom-patch/cli/account-limits/README.md +27 -0
  4. package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
  5. package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
  6. package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
  7. package/dist/axom-patch/cli/account-limits/generate.py +123 -0
  8. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +81 -0
  9. package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
  10. package/dist/axom-patch/cli/claude-account-limits.json +37 -0
  11. package/dist/axom-patch/cli/codex-account-limits.json +37 -0
  12. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  13. package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
  14. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  15. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  16. package/dist/axom-patch/cli/nightly-task.md +194 -0
  17. package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
  18. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +5 -1
  19. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  20. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  21. package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
  22. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
  23. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1591 -0
  24. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +360 -0
  25. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -5
  26. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -7
  27. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  28. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  29. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  30. package/dist/axom-patch/cli/runtime.json +39 -15
  31. package/dist/axom-patch/cli/zcode-account-limits.json +30 -0
  32. package/dist/axom-patch/cli/zcode-models.json +37 -0
  33. package/dist/axom-patch/cli/zcode-subagent-subwork.json +72 -0
  34. package/dist/cli.js +4 -9
  35. package/dist/commands/knowledge.js +47 -0
  36. package/dist/commands/update.js +65 -3
  37. package/dist/commands/workstation.js +73 -7
  38. package/dist/install/claude/commands.js +13 -13
  39. package/dist/keepers/acp/keeper.mjs +3083 -0
  40. package/dist/keepers/acp/manifest.json +9 -0
  41. package/dist/keepers/terminal/keeper.mjs +8469 -0
  42. package/dist/keepers/terminal/manifest.json +9 -0
  43. package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  44. package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  45. package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
  46. package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
  47. package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
  48. package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
  49. package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
  50. package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
  51. package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
  52. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
  53. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
  54. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
  55. package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  56. package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
  57. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  58. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  59. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  60. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  61. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  62. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  63. package/dist/mcp.js +1 -1
  64. package/dist/native/darwin-arm64/axom-quic +0 -0
  65. package/dist/test/keeperKit.js +67 -0
  66. package/dist/workstation/acp/acpAgents.js +2192 -636
  67. package/dist/workstation/acp/acpClient.js +84 -495
  68. package/dist/workstation/acp/adoption.js +10 -396
  69. package/dist/workstation/acp/binaryInstall.js +100 -27
  70. package/dist/workstation/acp/builds/gc.js +245 -0
  71. package/dist/workstation/acp/builds/install/binary.js +28 -0
  72. package/dist/workstation/acp/builds/install/npm.js +324 -0
  73. package/dist/workstation/acp/builds/install/uv.js +254 -0
  74. package/dist/workstation/acp/builds/migrate.js +195 -0
  75. package/dist/workstation/acp/builds/model.js +128 -0
  76. package/dist/workstation/acp/builds/nodeGate.js +100 -0
  77. package/dist/workstation/acp/builds/store.js +352 -0
  78. package/dist/workstation/acp/commandTableProbe.js +68 -0
  79. package/dist/workstation/acp/configOptions.js +103 -1
  80. package/dist/workstation/acp/envSecrets.js +27 -0
  81. package/dist/workstation/acp/initializeResponse.js +290 -0
  82. package/dist/workstation/acp/installErrors.js +12 -2
  83. package/dist/workstation/acp/installProbe.js +1 -1
  84. package/dist/workstation/acp/managedPatches.js +239 -159
  85. package/dist/workstation/acp/managedRuntime.js +314 -0
  86. package/dist/workstation/acp/outbox.js +330 -0
  87. package/dist/workstation/acp/patchAssets.js +60 -3
  88. package/dist/workstation/acp/permission.js +42 -10
  89. package/dist/workstation/acp/processor/checkpoint.js +78 -0
  90. package/dist/workstation/acp/processor/commands.js +35 -0
  91. package/dist/workstation/acp/processor/control.js +282 -0
  92. package/dist/workstation/acp/processor/emit.js +26 -0
  93. package/dist/workstation/acp/processor/jsonrpc.js +172 -0
  94. package/dist/workstation/acp/processor/memoryLog.js +49 -0
  95. package/dist/workstation/acp/processor/origins.js +48 -0
  96. package/dist/workstation/acp/processor/output.js +67 -0
  97. package/dist/workstation/acp/processor/projection.js +42 -0
  98. package/dist/workstation/acp/processor/record.js +128 -0
  99. package/dist/workstation/acp/processor/runtime.js +105 -0
  100. package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
  101. package/dist/workstation/acp/processor/session.js +496 -0
  102. package/dist/workstation/acp/processor/state.js +67 -0
  103. package/dist/workstation/acp/processor/step.js +442 -0
  104. package/dist/workstation/acp/processor/test/fixtureLog.js +58 -0
  105. package/dist/workstation/acp/processor/test/harness.js +120 -0
  106. package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
  107. package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
  108. package/dist/workstation/acp/processor/transport/generations.js +76 -0
  109. package/dist/workstation/acp/processor/transport/keeper.js +130 -0
  110. package/dist/workstation/acp/processor/transport/local.js +269 -0
  111. package/dist/workstation/acp/processor/transport/types.js +78 -0
  112. package/dist/workstation/acp/processor/updates.js +390 -0
  113. package/dist/workstation/acp/processorSession.js +1348 -0
  114. package/dist/workstation/acp/registry/catalog.js +189 -41
  115. package/dist/workstation/acp/registry/launch.js +76 -119
  116. package/dist/workstation/acp/sessionUpdateMapper.js +18 -7
  117. package/dist/workstation/acp/steering.js +58 -0
  118. package/dist/workstation/acp/switching.js +111 -0
  119. package/dist/workstation/acp/terminalLogin.js +16 -42
  120. package/dist/workstation/acp/terminalLoginSpec.js +41 -0
  121. package/dist/workstation/acp/test/adapterFixtures.js +256 -0
  122. package/dist/workstation/acp/test/daemonLife.js +106 -0
  123. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  124. package/dist/workstation/acp/test/keeperWorld.js +295 -0
  125. package/dist/workstation/acp/test/memoryOutbox.js +55 -0
  126. package/dist/workstation/acp/test/raw/fixtures.js +196 -0
  127. package/dist/workstation/acp/test/raw/sanitize.js +344 -0
  128. package/dist/workstation/acp/test/raw/sources.js +135 -0
  129. package/dist/workstation/acp/test/realAdapter.js +331 -0
  130. package/dist/workstation/acp/test/scriptAgent.js +214 -0
  131. package/dist/workstation/acp/test/sessionFrames.js +42 -0
  132. package/dist/workstation/acp/timedCommand.js +4 -2
  133. package/dist/workstation/acp/uvxInstall.js +72 -122
  134. package/dist/workstation/agentFlow.js +103 -0
  135. package/dist/workstation/agentSessions.js +1289 -544
  136. package/dist/workstation/channels/agent.js +7 -5
  137. package/dist/workstation/channels/computer.js +69 -21
  138. package/dist/workstation/channels/computerExecs.js +200 -0
  139. package/dist/workstation/channels/management.js +172 -8
  140. package/dist/workstation/channels/url.js +1 -1
  141. package/dist/workstation/config.js +16 -4
  142. package/dist/workstation/connection/encryptedChannel.js +4 -1
  143. package/dist/workstation/control/bindingStore.js +200 -0
  144. package/dist/workstation/control/commands.js +124 -0
  145. package/dist/workstation/control/daemonIdentity.js +53 -0
  146. package/dist/workstation/control/enrollment.js +106 -0
  147. package/dist/workstation/control/inspect.js +71 -0
  148. package/dist/workstation/control/locks.js +63 -0
  149. package/dist/workstation/control/protocol.js +40 -0
  150. package/dist/workstation/control/restartGuard.js +46 -0
  151. package/dist/workstation/control/runtimeClient.js +97 -0
  152. package/dist/workstation/control/runtimeHost.js +148 -0
  153. package/dist/workstation/control/serverIdentity.js +35 -0
  154. package/dist/workstation/daemon.js +393 -12
  155. package/dist/workstation/daemonctl.js +103 -14
  156. package/dist/workstation/desktopControl/audio.js +99 -0
  157. package/dist/workstation/desktopControl/capabilities.js +92 -0
  158. package/dist/workstation/desktopControl/captureModes.js +9 -0
  159. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  160. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  161. package/dist/workstation/desktopControl/helperProcess.js +353 -0
  162. package/dist/workstation/desktopControl/lobby.js +129 -0
  163. package/dist/workstation/desktopControl/macProvider.js +155 -0
  164. package/dist/workstation/desktopControl/media.js +331 -0
  165. package/dist/workstation/desktopControl/nativeProvider.js +1018 -0
  166. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  167. package/dist/workstation/desktopControl/overview.js +58 -0
  168. package/dist/workstation/desktopControl/protocol.js +302 -0
  169. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  170. package/dist/workstation/desktopControl/rateController.js +226 -0
  171. package/dist/workstation/desktopControl/supervisor.js +1681 -0
  172. package/dist/workstation/desktopControl/transport.js +27 -0
  173. package/dist/workstation/desktopControl/wallpaper.js +78 -0
  174. package/dist/workstation/exec/attach.js +126 -0
  175. package/dist/workstation/exec/command.js +40 -0
  176. package/dist/workstation/exec/durable.js +517 -0
  177. package/dist/workstation/exec/fallback.js +106 -0
  178. package/dist/workstation/exec/policy.js +837 -0
  179. package/dist/workstation/exec/shaping.js +211 -0
  180. package/dist/workstation/exec.js +3 -112
  181. package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
  182. package/dist/workstation/files/adapters/toolRequests.js +121 -0
  183. package/dist/workstation/files/client.js +455 -0
  184. package/dist/workstation/files/credentials.js +142 -0
  185. package/dist/workstation/files/errors.js +28 -0
  186. package/dist/workstation/files/fence.js +350 -0
  187. package/dist/workstation/files/find/fileIndex.js +153 -0
  188. package/dist/workstation/files/find/fuzzy.js +155 -0
  189. package/dist/workstation/files/host.js +122 -0
  190. package/dist/workstation/files/hostProtocol.js +2 -0
  191. package/dist/workstation/files/media/cache.js +620 -0
  192. package/dist/workstation/files/media/encoding.js +308 -0
  193. package/dist/workstation/files/media/estimator.js +100 -0
  194. package/dist/workstation/files/media/media.js +325 -0
  195. package/dist/workstation/files/media/packedAudio.js +23 -0
  196. package/dist/workstation/files/media/scheduler.js +551 -0
  197. package/dist/workstation/files/media/types.js +5 -0
  198. package/dist/workstation/files/scope.js +45 -0
  199. package/dist/workstation/files/structured.js +284 -0
  200. package/dist/workstation/{fileOps.js → files/text.js} +152 -39
  201. package/dist/workstation/files/uploads.js +351 -0
  202. package/dist/workstation/journal.js +65 -236
  203. package/dist/workstation/keepers/acp/client.js +101 -0
  204. package/dist/workstation/keepers/acp/envelope.js +62 -0
  205. package/dist/workstation/keepers/acp/host.js +419 -0
  206. package/dist/workstation/keepers/acp/main.js +18 -0
  207. package/dist/workstation/keepers/acp/memoryStream.js +121 -0
  208. package/dist/workstation/keepers/acp/params.js +137 -0
  209. package/dist/workstation/keepers/acp/protocol.js +66 -0
  210. package/dist/workstation/keepers/acp/recover.js +120 -0
  211. package/dist/workstation/keepers/acp/session.js +584 -0
  212. package/dist/workstation/keepers/acp/stderr.js +154 -0
  213. package/dist/workstation/keepers/kit/activity.js +103 -0
  214. package/dist/workstation/keepers/kit/bootstrap.js +164 -0
  215. package/dist/workstation/keepers/kit/buildVersion.js +51 -0
  216. package/dist/workstation/keepers/kit/bundle.js +124 -0
  217. package/dist/workstation/keepers/kit/client.js +432 -0
  218. package/dist/workstation/keepers/kit/identity.js +21 -0
  219. package/dist/workstation/keepers/kit/ipc.js +112 -0
  220. package/dist/workstation/keepers/kit/launchConfig.js +8 -0
  221. package/dist/workstation/keepers/kit/launcher.js +289 -0
  222. package/dist/workstation/keepers/kit/lock.js +123 -0
  223. package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
  224. package/dist/workstation/keepers/kit/paths.js +76 -0
  225. package/dist/workstation/keepers/kit/protocol.js +43 -0
  226. package/dist/workstation/keepers/kit/server.js +518 -0
  227. package/dist/workstation/keepers/kit/streamLog.js +697 -0
  228. package/dist/workstation/keepers/legacy.js +296 -0
  229. package/dist/workstation/keepers/policy.js +576 -0
  230. package/dist/workstation/keepers/stop.js +34 -0
  231. package/dist/workstation/keepers/terminal/client.js +213 -0
  232. package/dist/workstation/keepers/terminal/exec.js +981 -0
  233. package/dist/workstation/keepers/terminal/generations.js +129 -0
  234. package/dist/workstation/keepers/terminal/keeper.js +62 -0
  235. package/dist/workstation/keepers/terminal/main.js +43 -0
  236. package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
  237. package/dist/workstation/keepers/terminal/params.js +27 -0
  238. package/dist/workstation/keepers/terminal/protocol.js +56 -0
  239. package/dist/workstation/keepers/terminal/pty.js +330 -0
  240. package/dist/workstation/managedInstall.js +57 -0
  241. package/dist/workstation/protocol/accountLimits.js +47 -0
  242. package/dist/workstation/protocol/agent.js +50 -19
  243. package/dist/workstation/protocol/common.js +30 -0
  244. package/dist/workstation/protocol/computer.js +15 -4
  245. package/dist/workstation/protocol/management.js +141 -4
  246. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  247. package/dist/workstation/rd/artifact.js +119 -0
  248. package/dist/workstation/rd/control.js +119 -0
  249. package/dist/workstation/rd/display.js +29 -0
  250. package/dist/workstation/rd/provider.js +222 -0
  251. package/dist/workstation/rd/sessions.js +391 -0
  252. package/dist/workstation/rd/state.js +45 -0
  253. package/dist/workstation/rd/update.js +171 -0
  254. package/dist/workstation/rd/upkeep.js +38 -0
  255. package/dist/workstation/register.js +27 -37
  256. package/dist/workstation/service/installer.js +104 -13
  257. package/dist/workstation/service/launchd.js +1 -0
  258. package/dist/workstation/storageMetrics.js +118 -0
  259. package/dist/workstation/systemMetrics.js +154 -93
  260. package/dist/workstation/toolchain.js +94 -0
  261. package/dist/workstation/tools/browser.js +28 -4
  262. package/dist/workstation/tools/client.js +9 -1
  263. package/dist/workstation/tools/info.js +50 -0
  264. package/dist/workstation/tools/keeperProcess.js +15 -5
  265. package/dist/workstation/tools/router.js +55 -0
  266. package/dist/workstation/tools/terminalRouter.js +222 -0
  267. package/dist/workstation/tools/validation.js +37 -1
  268. package/dist/workstation/update/artifact.js +2 -1
  269. package/dist/workstation/update/coordinator.js +3 -0
  270. package/dist/workstation/update/globalInstall.js +116 -0
  271. package/dist/workstation/update/npmInstaller.js +49 -23
  272. package/dist/workstation/update/ptyPreflight.js +21 -0
  273. package/dist/workstation/update/rdRollback.js +145 -0
  274. package/dist/workstation/update/runUpdate.js +3 -2
  275. package/dist/workstation/update/taskRegistry.js +105 -11
  276. package/dist/workstation/update/watchdog.js +16790 -334
  277. package/package.json +11 -8
  278. package/skills/axom/SKILL.md +11 -11
  279. package/skills/axom-definition/SKILL.md +3 -3
  280. package/skills/axom-platform/SKILL.md +3 -3
  281. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  282. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -49
  283. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  284. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  285. package/dist/commands/guide.js +0 -38
  286. package/dist/workstation/acp/acpSession.js +0 -1475
  287. package/dist/workstation/acp/contract.js +0 -499
  288. package/dist/workstation/acp/delegationMcp.js +0 -24
  289. package/dist/workstation/acp/draftInterceptor.js +0 -163
  290. package/dist/workstation/acp/qoderStartup.js +0 -45
  291. package/dist/workstation/acp/sessionScope.js +0 -19
  292. package/dist/workstation/filePaths.js +0 -174
  293. package/dist/workstation/fileWorker.js +0 -81
  294. package/dist/workstation/fileWorkerClient.js +0 -121
  295. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -106
  296. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -262
  297. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  298. package/dist/workstation/qoderAdapter/contract.js +0 -78
  299. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -30
  300. package/dist/workstation/qoderAdapter/history.js +0 -87
  301. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  302. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  303. package/dist/workstation/qoderAdapter/main.js +0 -93
  304. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  305. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  306. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  307. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  308. package/dist/workstation/qoderAdapter/profile.js +0 -93
  309. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -49
  310. package/dist/workstation/qoderAdapter/session.js +0 -319
  311. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  312. package/dist/workstation/qoderAdapter/subwork.js +0 -77
  313. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  314. package/dist/workstation/qoderAdapter/taskEvent.js +0 -26
  315. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  316. package/dist/workstation/qoderAdapter/usage.js +0 -37
  317. package/dist/workstation/tools/core.js +0 -50
  318. package/dist/workstation/tools/files.js +0 -525
  319. package/dist/workstation/tools/terminal.js +0 -214
  320. /package/dist/workstation/{qoderAdapter/types.js → desktopControl/provider.js} +0 -0
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import semver from 'semver';
4
+ import { assertSafePackageName } from './builds/model.js';
4
5
  // Production reads only assets shipped in this CLI. The .ts path is for source-based tests.
5
6
  export const patchAssetRoot = new URL(import.meta.url.endsWith('.ts') ? '../../../../axom-patch/cli/' : '../../axom-patch/cli/', import.meta.url);
6
7
  const runtime = JSON.parse(readFileSync(new URL('runtime.json', patchAssetRoot), 'utf8'));
@@ -20,9 +21,16 @@ function validateCondition(id, when, until, whenKey, untilKey) {
20
21
  export function validateOverride(entry) {
21
22
  validateCondition(entry.id, entry.when_registry_version, entry.until_registry_version, 'when_registry_version', 'until_registry_version');
22
23
  const until = entry.until_registry_version;
23
- // A pin at or past its own bound would be replaced by the registry the moment it applied.
24
- if (until !== undefined && entry.distribution && !(semver.valid(entry.distribution.version) && semver.lt(entry.distribution.version, until))) {
25
- throw new Error(`Axom patch ${entry.id}: pinned ${entry.distribution.version} must sort below ${until}`);
24
+ // A catch-up pin may equal its bound: use it until the registry publishes that release.
25
+ // A pin above the bound would instead let the registry replace it with an older release.
26
+ if (until !== undefined && entry.distribution && !(semver.valid(entry.distribution.version) && semver.lte(entry.distribution.version, until))) {
27
+ throw new Error(`Axom patch ${entry.id}: pinned ${entry.distribution.version} must not sort above ${until}`);
28
+ }
29
+ }
30
+ /** Portable relative paths only: no platform-specific interpretation or normalization. */
31
+ export function validatePatchPath(value) {
32
+ if (typeof value !== 'string' || !value || /[\\:\0]/.test(value) || value.split('/').some(part => !part || part === '.' || part === '..')) {
33
+ throw new Error('Axom patch path escapes its root or is not a portable relative path');
26
34
  }
27
35
  }
28
36
  export function validatePackagePatch(recipe) {
@@ -30,6 +38,16 @@ export function validatePackagePatch(recipe) {
30
38
  if (recipe.versions === undefined && recipe.until_version === undefined) {
31
39
  throw new Error(`Axom patch ${recipe.id}: needs versions or until_version`);
32
40
  }
41
+ for (const edit of recipe.edits)
42
+ validatePatchPath(edit.file);
43
+ if (recipe.assets !== undefined && !Array.isArray(recipe.assets))
44
+ throw new Error('Axom patch assets must be an array');
45
+ for (const asset of recipe.assets ?? []) {
46
+ validatePatchPath(asset.source);
47
+ validatePatchPath(asset.target);
48
+ if (!/^[a-f0-9]{64}$/.test(asset.sha256))
49
+ throw new Error('Axom patch asset sha256 must be a lowercase SHA-256 digest');
50
+ }
33
51
  }
34
52
  function inRange(version, when, until) {
35
53
  if (when)
@@ -61,3 +79,42 @@ export const packagePatches = runtime.package_patches.map((file) => {
61
79
  validatePackagePatch(recipe);
62
80
  return recipe;
63
81
  });
82
+ /** The declared native wrappers, for the catalog↔patch consistency guard in `catalog.test.ts`. */
83
+ export const nativeExtensions = runtime.native_extensions ?? [];
84
+ /** The bundled overrides, for the same catalog↔patch consistency guard. */
85
+ export const runtimeOverrides = runtime.overrides;
86
+ /** Native wrappers are declared in the patch inventory, never selected by agent-specific launch branches. */
87
+ export function nativeExtensionFor(agent, version) {
88
+ const extension = runtime.native_extensions?.find(entry => entry.agent === agent && entry.versions.includes(version));
89
+ if (!extension)
90
+ return undefined;
91
+ if (extension.entry.startsWith('/') || extension.entry.split('/').includes('..'))
92
+ throw new Error('Invalid native extension path');
93
+ return new URL(extension.entry, patchAssetRoot);
94
+ }
95
+ export function nativeAccountLimitsVersionFor(agent, version) {
96
+ return runtime.native_extensions?.find(entry => entry.agent === agent && entry.versions.includes(version))?.accountLimitsVersion ?? 2;
97
+ }
98
+ /** One entry per package; the only policy is skipping npm lifecycle scripts a package would run in the user's HOME. */
99
+ export function validateInstallPolicies(policies) {
100
+ if (!Array.isArray(policies))
101
+ throw new Error('Axom install_policies must be an array');
102
+ const seen = new Set();
103
+ for (const policy of policies) {
104
+ if (typeof policy?.package !== 'string')
105
+ throw new Error('Axom install policy needs a package name');
106
+ assertSafePackageName(policy.package);
107
+ if (seen.has(policy.package))
108
+ throw new Error(`Axom install policy for ${policy.package} is duplicated`);
109
+ if (policy.ignore_scripts !== true)
110
+ throw new Error(`Axom install policy for ${policy.package} must set ignore_scripts: true`);
111
+ seen.add(policy.package);
112
+ }
113
+ return policies;
114
+ }
115
+ export const installPolicies = validateInstallPolicies(runtime.install_policies ?? []);
116
+ export function installPolicyFor(name, policies = installPolicies) {
117
+ return policies.find((policy) => policy.package === name);
118
+ }
119
+ /** Optional strict release policies; activation is an explicit bundled manifest change. */
120
+ export const managedRuntimePolicies = runtime.managed_runtimes ?? [];
@@ -78,7 +78,6 @@
78
78
  * complete it, so `authenticate` remembers every id it handed out and retracts what is left when
79
79
  * the login settles.
80
80
  */
81
- import { randomUUID } from 'node:crypto';
82
81
  import { methods } from '@agentclientprotocol/sdk';
83
82
  import { validateWireEvent } from './validate.js';
84
83
  /** A value guaranteed not to equal any real `optionId` in `input.options` for a given call — used to purge a pending entry without accidentally supplying a matchable answer. Regenerated per call rather than a fixed literal, for the same anti-collision reasoning as `AcpSession`'s own abandonment sentinel (review finding, task-5 item 6). */
@@ -123,8 +122,9 @@ function toolCallPayload(toolCall) {
123
122
  * ACP `RequestPermissionRequest` (`types.gen.d.ts:108`) on Axom's wire: `{toolCall, options}` plus
124
123
  * `requestId`.
125
124
  *
126
- * `requestId` is a host-generated resolver token. The raw JSON-RPC ID stays local to its
127
- * connection; exact `sessionId` travels in the wire event's `acpSessionId` origin field.
125
+ * `requestId` is a host-derived resolver token — {@link permissionRequestId}, the same on the live
126
+ * path and in the ACP processor. The raw JSON-RPC ID stays local to its connection; exact `sessionId`
127
+ * travels in the wire event's `acpSessionId` origin field.
128
128
  *
129
129
  * **`toolCall` is nested, not flattened.** The flattened version lifted the tool's
130
130
  * `name`/`title`/`kind`/`arguments` to the top level, which put `name` (the tool's programmatic name,
@@ -135,7 +135,7 @@ function toolCallPayload(toolCall) {
135
135
  * `toolCall.title`, and the renderer computes it from the same field now
136
136
  * (`WorkTimelineInteractions.foldPermission`) instead of storing a second, drifting copy.
137
137
  */
138
- function buildPermissionRequestedEvent(input) {
138
+ export function buildPermissionRequestedEvent(input) {
139
139
  return {
140
140
  eventName: 'permission_request',
141
141
  ...(input.sessionId !== undefined ? { acpSessionId: input.sessionId } : {}),
@@ -164,7 +164,7 @@ function buildPermissionRequestedEvent(input) {
164
164
  * `sink.answer` — the agent gave up on this specific request, so nothing should still expect an
165
165
  * answer for it. Session-level abandonment (the session closing, or the connection dying) is instead
166
166
  * `AcpSession`'s own job (`registerPendingApproval` resolving immediately when already closed, and
167
- * `runReadLoop` draining on an aborted connection) — see `acpSession.ts`.
167
+ * `runReadLoop` draining on an aborted connection) — see the old `acpSession.ts` (deleted in Task 29).
168
168
  *
169
169
  * The eventual answer is whatever string reaches `sink`'s registered resolver: normally one of
170
170
  * `input.options`' own `optionId`s (relayed from the user through `AcpSession.answer`), but both
@@ -320,7 +320,7 @@ function isFoldedCompanionKey(key, properties) {
320
320
  * each return `null` from `elicitationRequestFromPayload`, because `MODE_FORM` requires
321
321
  * `fields.isNotEmpty()`. Everything else it now decodes (see {@link handleFormElicitation}).
322
322
  */
323
- function elicitationFieldKeys(requestedSchema) {
323
+ export function elicitationFieldKeys(requestedSchema) {
324
324
  const properties = requestedSchema?.properties;
325
325
  if (properties === null || typeof properties !== 'object' || Array.isArray(properties))
326
326
  return [];
@@ -336,7 +336,7 @@ function elicitationFieldKeys(requestedSchema) {
336
336
  * request is dropped server-side, which is why this check survived Task 9's removal of the
337
337
  * per-property profile predicate (see the ruling-R24 block below).
338
338
  */
339
- function isBlank(value) {
339
+ export function isBlank(value) {
340
340
  return value.trim().length === 0;
341
341
  }
342
342
  /**
@@ -406,7 +406,7 @@ function isBlank(value) {
406
406
  * event always carries exactly what the agent asked; {@link handleFormElicitation} is what decides,
407
407
  * from that same schema, whether to wait for an answer or decline right after raising the card.
408
408
  */
409
- function buildElicitationRequestedEvent(request, requestId) {
409
+ export function buildElicitationRequestedEvent(request, requestId) {
410
410
  return {
411
411
  eventName: 'elicitation_request',
412
412
  payload: {
@@ -521,6 +521,35 @@ async function handleFormElicitation(request, ctx, sink) {
521
521
  export function handleElicitationComplete(notification, deps) {
522
522
  deps.dismiss(notification.elicitationId);
523
523
  }
524
+ /**
525
+ * The card id of one `session/request_permission`: `perm:<hostGeneration>:<JSON agent id>` (spec
526
+ * 2026-10-11 §5.4).
527
+ *
528
+ * ## Why derived, not random
529
+ *
530
+ * It used to be `randomUUID()` per invocation. A card id the server is already showing must still
531
+ * resolve after the daemon restarts underneath a live adapter, and a random id cannot: the re-presented
532
+ * request would get a new one, `settleApproval` ignores unknown ids, and the user's answer to the
533
+ * visible card would be lost (analysis A §4). Derived from the request itself, the same request gets
534
+ * the same id in every daemon generation, and the ACP processor — which may not draw randomness at
535
+ * all (`processor/guard.test.ts`) — computes it with this very function.
536
+ *
537
+ * ## Why it is still unique
538
+ *
539
+ * - `hostGeneration` identifies one adapter process: the keeper's `spawnId` on the processor path,
540
+ * `AgentRuntimeDiagnostics.generation` (fresh per `connectAgent`) on today's live path — the same
541
+ * value published as `callbackContext.hostGeneration`. A reconnect is a new process, so a new
542
+ * generation: the agent's ids restart at 0 there without colliding with an earlier card.
543
+ * - Within one process the agent never reuses a pending JSON-RPC id. `JSON.stringify` keeps the
544
+ * number `1` and the string `"1"` apart.
545
+ *
546
+ * Kept short enough for `AcpSession.registerPendingApproval`'s 256-byte rule for every id a real
547
+ * adapter sends; a pathological id longer than ~200 bytes is refused there, as an overlong
548
+ * `rpcRequestId` already was.
549
+ */
550
+ export function permissionRequestId(hostGeneration, agentRequestId) {
551
+ return `perm:${hostGeneration}:${JSON.stringify(agentRequestId)}`;
552
+ }
524
553
  /**
525
554
  * Register this module's three handlers onto `app`. Must run inside `connectAgent`'s `configure`
526
555
  * hook — see the file doc comment for why a post-connect `.onRequest`/`.onNotification` would compile
@@ -534,15 +563,18 @@ export function handleElicitationComplete(notification, deps) {
534
563
  * the call site instead. Its own `present` may still be omitted (see {@link PresentElicitationUrl}) —
535
564
  * that is `acpAgents.ts`'s session connection saying, on purpose, that a `url` elicitation raised
536
565
  * outside a login has nowhere to go, and it declines rather than accepting into nothing.
566
+ *
567
+ * `connection.generation` is the adapter process's generation — `connectAgent` hands it to its
568
+ * `configure` hook — and scopes the card ids {@link permissionRequestId} derives.
537
569
  */
538
- export function registerPermissionHandlers(app, sink, elicitationDeps) {
570
+ export function registerPermissionHandlers(app, sink, elicitationDeps, connection) {
539
571
  app
540
572
  .onRequest(methods.client.session.requestPermission, async (ctx) => {
541
573
  if (typeof ctx.params.sessionId !== 'string' || !ctx.params.sessionId.trim()) {
542
574
  console.error(JSON.stringify({ component: 'acp', error: 'invalid_permission_origin', rpcRequestId: String(ctx.requestId) }));
543
575
  return { outcome: { outcome: 'cancelled' } };
544
576
  }
545
- const decision = await handleRequestPermission({ requestId: randomUUID(), rpcRequestId: String(ctx.requestId), sessionId: ctx.params.sessionId, toolCall: ctx.params.toolCall, options: ctx.params.options, signal: ctx.signal }, sink);
577
+ const decision = await handleRequestPermission({ requestId: permissionRequestId(connection.generation, ctx.requestId), rpcRequestId: String(ctx.requestId), sessionId: ctx.params.sessionId, toolCall: ctx.params.toolCall, options: ctx.params.options, signal: ctx.signal }, sink);
546
578
  return {
547
579
  outcome: decision.outcome === 'answered' ? { outcome: 'selected', optionId: decision.optionId } : { outcome: 'cancelled' },
548
580
  };
@@ -0,0 +1,78 @@
1
+ /** Checkpoint schema answers whether state is readable; processorVersion identifies output semantics. Readable older processor versions remain restorable; replay differences across versions do not count as divergence. Version 9 moves adapter-specific compatibility folds out and removes retired optional state. */
2
+ import { initialState, PROCESSOR_STATE_SCHEMA } from './state.js';
3
+ /**
4
+ * Output history: 2 — turns, cards, seq and semantic-only request checks;
5
+ * 3 — session updates, binding, config, steering and validated events;
6
+ * 4 — spawn-bound commands and history dropped counts; prompt closes history;
7
+ * 5 — callback snapshots, runtime revisions and adapter compatibility folds;
8
+ * 6 — child status precedes process results; ended calls release data;
9
+ * 7 — install outcomes become step events;
10
+ * 8 — loaded-session levels fold live; output gaps become steps;
11
+ * 9 — adapter compatibility folds moved out and retired optional state removed.
12
+ */
13
+ export const PROCESSOR_VERSION = 9;
14
+ export const CHECKPOINT_SCHEMA = 1;
15
+ export class CheckpointUnreadableError extends Error {
16
+ }
17
+ function clone(value) {
18
+ return JSON.parse(JSON.stringify(value));
19
+ }
20
+ /** A detached copy of `state`, so later steps cannot reach into a checkpoint already handed out. */
21
+ export function checkpointOf(state) {
22
+ return { schema: CHECKPOINT_SCHEMA, processorVersion: PROCESSOR_VERSION, offset: state.offset, state: clone(state) };
23
+ }
24
+ export function encodeCheckpoint(checkpoint) {
25
+ return JSON.stringify(checkpoint);
26
+ }
27
+ function isRecord(value) {
28
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
29
+ }
30
+ /** Parse and check a checkpoint blob (string or already-parsed JSON). */
31
+ export function decodeCheckpoint(blob) {
32
+ let value = blob;
33
+ if (typeof blob === 'string') {
34
+ try {
35
+ value = JSON.parse(blob);
36
+ }
37
+ catch {
38
+ throw new CheckpointUnreadableError('checkpoint is not JSON');
39
+ }
40
+ }
41
+ if (!isRecord(value))
42
+ throw new CheckpointUnreadableError('checkpoint is not an object');
43
+ if (value.schema !== CHECKPOINT_SCHEMA)
44
+ throw new CheckpointUnreadableError(`unknown checkpoint schema ${JSON.stringify(value.schema)}`);
45
+ const { processorVersion, offset, state } = value;
46
+ if (typeof processorVersion !== 'number' || !Number.isInteger(processorVersion))
47
+ throw new CheckpointUnreadableError('checkpoint has no processorVersion');
48
+ if (typeof offset !== 'number' || !Number.isSafeInteger(offset) || offset < -1)
49
+ throw new CheckpointUnreadableError('checkpoint offset is invalid');
50
+ if (!isRecord(state) || (state.schema !== 4 && state.schema !== PROCESSOR_STATE_SCHEMA))
51
+ throw new CheckpointUnreadableError('checkpoint state schema is unknown');
52
+ if (state.offset !== offset)
53
+ throw new CheckpointUnreadableError('checkpoint offset disagrees with its state');
54
+ for (const key of ['process', 'initialize', 'outbound', 'inbound', 'diagnostics', 'permissions', 'elicitations',
55
+ 'permissionOrigins', 'permissionParents', 'unresolvedPermissionOrigins', 'open', 'mapperScopes', 'message', 'steers',
56
+ 'asyncTasks', 'runtime', 'callbacks']) {
57
+ if (!isRecord(state[key]))
58
+ throw new CheckpointUnreadableError(`checkpoint state has no ${key}`);
59
+ }
60
+ if (typeof state.seq !== 'number' || !Array.isArray(state.turns) || !Array.isArray(state.settledPermissions)) {
61
+ throw new CheckpointUnreadableError('checkpoint state has no session layer (seq/turns)');
62
+ }
63
+ if (!Array.isArray(state.steered) || !('configOptions' in state) || !('history' in state)
64
+ || !('activity' in state)) {
65
+ throw new CheckpointUnreadableError('checkpoint state has no open-session layer (Task 17b)');
66
+ }
67
+ return { schema: CHECKPOINT_SCHEMA, processorVersion, offset, state: clone({ ...(state.schema === 4 ? upgradeStateFromSchema4(state) : state), ...(!Object.hasOwn(state, 'draftSubagents') ? { draftSubagents: { children: {}, overflow: false } } : {}) }) };
68
+ }
69
+ /** Read only the current neutral keys; retired adapter state is deliberately not copied. */
70
+ function upgradeStateFromSchema4(state) {
71
+ const keys = [...Object.keys(initialState()), 'closed', 'accountAuthBillingModes', 'accountLimits'];
72
+ const current = Object.fromEntries(keys.filter((key) => Object.hasOwn(state, key)).map((key) => [key, state[key]]));
73
+ const runtime = state.runtime;
74
+ return { ...current, schema: PROCESSOR_STATE_SCHEMA, runtime: {
75
+ revision: runtime.revision, available: runtime.available,
76
+ ...(Object.hasOwn(runtime, 'launch') ? { launch: runtime.launch } : {}),
77
+ } };
78
+ }
@@ -0,0 +1,35 @@
1
+ /** The line to write for `command`: `fill`ed from `resolve`, or `undefined` if its blocks are gone. */
2
+ export function materializeLine(command, resolve) {
3
+ if (command.fill === undefined)
4
+ return command.line;
5
+ const blocks = resolve?.(command.fill.blocksRef);
6
+ if (blocks === undefined)
7
+ return undefined;
8
+ const message = JSON.parse(command.line);
9
+ // Assigning an existing key keeps its position: the line keeps the SDK's key order.
10
+ message.params.prompt = blocks;
11
+ return JSON.stringify(message);
12
+ }
13
+ export function executeDownCommands(sink, commands, resolve) {
14
+ const written = [];
15
+ const dropped = [];
16
+ const refused = [];
17
+ const unresolved = [];
18
+ for (const command of commands) {
19
+ if (sink.hasDown(command.cause)) {
20
+ dropped.push(command);
21
+ continue;
22
+ }
23
+ const line = materializeLine(command, resolve);
24
+ if (line === undefined) {
25
+ unresolved.push(command);
26
+ continue;
27
+ }
28
+ const { fill: _fill, ...rest } = command;
29
+ if (sink.writeDown({ ...rest, line }) === 'refused')
30
+ refused.push(command);
31
+ else
32
+ written.push(command);
33
+ }
34
+ return { written, dropped, refused, unresolved };
35
+ }
@@ -0,0 +1,282 @@
1
+ /** Open-session binding, configuration, steering and launch notes. A response binds the native root; a failed load never silently creates another session here. The daemon supplies the finished MCP list with secrets replaced by references. */
2
+ import { newSessionMetaFor } from '../activityReporting.js';
3
+ import { flatChoicesOf, modesAlreadyRepresented, modelsAlreadyRepresented, normalizeConfigOptions, SYNTHESIZED_MODES_CONFIG_ID, SYNTHESIZED_MODEL_CONFIG_ID } from '../configOptions.js';
4
+ import { ASYNC_TASK_STOP_METHOD, mapSteerOutcome, STEER_METHOD } from '../steering.js';
5
+ import { emitEvent, isRecord } from './emit.js';
6
+ import { settlementErrorCode } from './jsonrpc.js';
7
+ import { sameCause } from './record.js';
8
+ import { onLaunchNote } from './runtime.js';
9
+ import { bindRootSession, closeErrorText, connected } from './session.js';
10
+ import { getOwn, putOwn, STEERED_HISTORY_LIMIT, } from './state.js';
11
+ import { endHistory } from './updates.js';
12
+ const OPEN_METHODS = { new: 'session/new', load: 'session/load', resume: 'session/resume' };
13
+ export const NOT_INITIALIZED = 'The ACP connection has not finished initialize';
14
+ export const ALREADY_OPEN = 'An ACP session is already open on this connection';
15
+ export const NOT_OPEN = 'No ACP session is open on this connection';
16
+ function request(state, out, method, params, kind, ctx, extras = {}) {
17
+ const { id, cause } = out.request(method, params, extras);
18
+ const pending = { id, method, kind, cause, ...(ctx !== undefined ? { ctx } : {}) };
19
+ state.outbound[String(id)] = pending;
20
+ return pending;
21
+ }
22
+ function isStringArray(value) {
23
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
24
+ }
25
+ // ---- open ---------------------------------------------------------------------------------------
26
+ export function onOpenNote(state, note, out) {
27
+ const { mode, cwd, sessionId, mcpServers, redact } = note;
28
+ if ((mode !== 'new' && mode !== 'load' && mode !== 'resume') || typeof cwd !== 'string' || cwd === ''
29
+ || (mode === 'new' ? sessionId !== undefined : typeof sessionId !== 'string' || sessionId === '')
30
+ || (mcpServers !== undefined && !Array.isArray(mcpServers)) || (redact !== undefined && !isStringArray(redact))) {
31
+ state.diagnostics.droppedNotes += 1;
32
+ return;
33
+ }
34
+ const refuse = (error) => out.unjournaled({ t: 'processor.open_result', ok: false, mode, error });
35
+ if (!connected(state))
36
+ return refuse(closeErrorText(state));
37
+ if (state.initialize.status !== 'ready')
38
+ return refuse(NOT_INITIALIZED);
39
+ if (state.open.status === 'pending' || state.open.status === 'open')
40
+ return refuse(ALREADY_OPEN);
41
+ const meta = mode === 'resume' ? undefined : newSessionMetaFor(state.initialize.facts.agentInfo?.name);
42
+ const servers = mcpServers ?? [];
43
+ const params = mode === 'new'
44
+ ? { cwd, mcpServers: servers, ...(meta ? { _meta: meta } : {}) }
45
+ : mode === 'load'
46
+ ? { sessionId, cwd, mcpServers: servers, ...(meta ? { _meta: meta } : {}) }
47
+ : { sessionId, cwd, mcpServers: servers };
48
+ const pending = request(state, out, OPEN_METHODS[mode], params, 'open', { mode, ...(sessionId !== undefined ? { sessionId } : {}) }, redact !== undefined && redact.length > 0 ? { redact } : {});
49
+ state.open = { status: 'pending', requestId: pending.id, mode };
50
+ if (mode === 'load')
51
+ state.history = { rootSessionId: sessionId, scopes: {}, messages: [], droppedCount: 0 };
52
+ }
53
+ /** The open request settled: on success the response binds the root session, now. */
54
+ export function settleOpen(state, entry, settlement, out) {
55
+ const ctx = entry.ctx;
56
+ if (state.open.status !== 'pending' || state.open.requestId !== entry.id)
57
+ return;
58
+ const { mode } = ctx;
59
+ if (settlement.outcome !== 'result') {
60
+ const code = settlementErrorCode(settlement);
61
+ const error = settlement.outcome === 'error' ? settlement.error.message : closeErrorText(state);
62
+ state.open = { status: 'failed', mode, error, ...(code !== undefined ? { code } : {}) };
63
+ endHistory(state, out, false);
64
+ // A failed open closes the attached session, and its repair with it (`AcpSession.close`).
65
+ out.unjournaled({ t: 'processor.open_result', ok: false, mode, error, ...(code !== undefined ? { code } : {}) });
66
+ return;
67
+ }
68
+ const result = isRecord(settlement.result) ? settlement.result : {};
69
+ const nativeSessionId = mode === 'new' ? (typeof result.sessionId === 'string' ? result.sessionId : '') : ctx.sessionId;
70
+ if (nativeSessionId.length > 0) {
71
+ bindRootSession(state, nativeSessionId);
72
+ }
73
+ state.configOptions = normalizeConfigOptions(result.configOptions, result.modes, result.models);
74
+ state.open = { status: 'open', mode };
75
+ out.unjournaled({
76
+ t: 'processor.open_result', ok: true, mode,
77
+ nativeSessionId: nativeSessionId.length > 0 ? nativeSessionId : null,
78
+ configOptions: state.configOptions,
79
+ });
80
+ }
81
+ /** `historyEnd{}` — the daemon's decision that a load's history is over (analysis B A4). */
82
+ export function onHistoryEndNote(state, out) {
83
+ if (!state.history) {
84
+ state.diagnostics.droppedNotes += 1;
85
+ return;
86
+ }
87
+ endHistory(state, out, true);
88
+ }
89
+ /** Usable for a session request: connected, and the open succeeded with a root. */
90
+ function sessionReady(state) {
91
+ if (!connected(state))
92
+ return closeErrorText(state);
93
+ if (state.open.status !== 'open' || state.rootSessionId === null)
94
+ return NOT_OPEN;
95
+ return undefined;
96
+ }
97
+ /** `agent_set_config_option{id, configId, value}` — `setConfigOptionRequest`'s request half. */
98
+ export function onSetConfigOptionNote(state, note, out) {
99
+ const { id, configId, value } = note;
100
+ if (typeof id !== 'string' || typeof configId !== 'string' || (typeof value !== 'string' && typeof value !== 'boolean')) {
101
+ state.diagnostics.droppedNotes += 1;
102
+ return;
103
+ }
104
+ const fail = (error) => out.unjournaled({ t: 'agent_config_option_set', id, ok: false, error });
105
+ const unusable = sessionReady(state);
106
+ if (unusable !== undefined)
107
+ return fail(unusable);
108
+ const sessionId = state.rootSessionId;
109
+ const ctx = { id, configId, value };
110
+ if (configId === SYNTHESIZED_MODES_CONFIG_ID) {
111
+ if (typeof value !== 'string')
112
+ return fail(`[acp] ${SYNTHESIZED_MODES_CONFIG_ID} takes a mode id (a string), got ${typeof value}`);
113
+ request(state, out, 'session/set_mode', { sessionId, modeId: value }, 'config', ctx);
114
+ return;
115
+ }
116
+ // 合成的模型控件走 unstable 的专用 RPC,与 set_mode 同一条保留 id 的路由规则。hermes 回合进行中
117
+ // 会拒绝("Session is busy"),那正是 settle 的错误支要说的话。
118
+ if (configId === SYNTHESIZED_MODEL_CONFIG_ID) {
119
+ if (typeof value !== 'string')
120
+ return fail(`[acp] ${SYNTHESIZED_MODEL_CONFIG_ID} takes a model id (a string), got ${typeof value}`);
121
+ request(state, out, 'session/set_model', { sessionId, modelId: value }, 'config', ctx);
122
+ return;
123
+ }
124
+ request(state, out, 'session/set_config_option', typeof value === 'boolean' ? { sessionId, configId, type: 'boolean', value } : { sessionId, configId, value }, 'config', ctx);
125
+ }
126
+ /** The set settled — `setConfigOptionRequest`'s snapshot rules, then `agent_config_option_set`. */
127
+ export function settleConfig(state, entry, settlement, out) {
128
+ const { id, configId, value } = entry.ctx;
129
+ if (settlement.outcome !== 'result') {
130
+ out.unjournaled({ t: 'agent_config_option_set', id, ok: false,
131
+ error: settlement.outcome === 'error' ? settlement.error.message : closeErrorText(state) });
132
+ return;
133
+ }
134
+ const current = state.configOptions ?? [];
135
+ let next;
136
+ if (entry.method === 'session/set_mode') {
137
+ next = current.map((option) => (option.id === configId && option.type === 'select' ? { ...option, currentValue: value } : option));
138
+ }
139
+ else if (entry.method === 'session/set_model') {
140
+ // hermes 的 SetSessionModelResponse 是空对象(切完不回快照、也不推更新),与 set_mode 同款:
141
+ // 本地把 currentValue 换成刚设的值;agent 若将来回了新状态,也只会被下一次 open 快照纠回真相。
142
+ next = current.map((option) => (option.id === configId && option.type === 'select' ? { ...option, currentValue: value } : option));
143
+ }
144
+ else {
145
+ const result = isRecord(settlement.result) ? settlement.result : {};
146
+ const reNormalized = normalizeConfigOptions(result.configOptions, null);
147
+ // 两个合成控件(modes、model)都不在 agent 自己的 configOptions 里,re-normalize 会把它们丢掉;
148
+ // 只要 agent 的新快照里还没有能替代它们的控件,就原样携带。
149
+ next = reNormalized.concat(current.filter((option) => (option.id === SYNTHESIZED_MODES_CONFIG_ID
150
+ && !modesAlreadyRepresented(reNormalized, flatChoicesOf(option) ?? []))
151
+ || (option.id === SYNTHESIZED_MODEL_CONFIG_ID && !modelsAlreadyRepresented(reNormalized))));
152
+ }
153
+ state.configOptions = next;
154
+ out.unjournaled({ t: 'agent_config_option_set', id, ok: true, configOptions: next });
155
+ }
156
+ // ---- steering -----------------------------------------------------------------------------------
157
+ /** `agent_steer{id, blocksRef, priority?}` — `steerRequest`, blocks filled by the executor. */
158
+ export function onSteerNote(state, note, out) {
159
+ const { id, blocksRef, priority } = note;
160
+ if (typeof id !== 'string' || id === '' || typeof blocksRef !== 'string' || blocksRef === ''
161
+ || (priority !== undefined && typeof priority !== 'string')) {
162
+ state.diagnostics.droppedNotes += 1;
163
+ return;
164
+ }
165
+ const unusable = sessionReady(state);
166
+ if (unusable !== undefined) {
167
+ out.unjournaled({ t: 'agent_steered', id, ok: false, error: unusable });
168
+ return;
169
+ }
170
+ if (getOwn(state.steers, id)) {
171
+ state.diagnostics.droppedNotes += 1;
172
+ return;
173
+ }
174
+ const pending = request(state, out, STEER_METHOD, {
175
+ sessionId: state.rootSessionId,
176
+ prompt: [],
177
+ _meta: { steering: { idleBehavior: 'promptRequired', ...(priority === undefined ? {} : { priority }) } },
178
+ }, 'steer', { id }, { fill: { blocksRef } });
179
+ putOwn(state.steers, id, { requestId: pending.id, cause: pending.cause, sent: false });
180
+ }
181
+ /** A `down` record: a queued steer's line reached the agent. */
182
+ export function onSteerDown(state, record) {
183
+ const cause = record.annotation?.cause;
184
+ if (!cause)
185
+ return;
186
+ for (const steer of Object.values(state.steers)) {
187
+ if (!steer.sent && sameCause(cause, steer.cause))
188
+ steer.sent = true;
189
+ }
190
+ }
191
+ function rememberSteered(state, id) {
192
+ state.steered.push(id);
193
+ if (state.steered.length > STEERED_HISTORY_LIMIT)
194
+ state.steered.splice(0, state.steered.length - STEERED_HISTORY_LIMIT);
195
+ }
196
+ /** The steer settled: `agent_steered`, with `mapSteerOutcome`'s narrowing (a throw is a failed reply). */
197
+ export function settleSteer(state, entry, settlement, out) {
198
+ const { id } = entry.ctx;
199
+ delete state.steers[id];
200
+ if (settlement.outcome !== 'result') {
201
+ out.unjournaled({ t: 'agent_steered', id, ok: false,
202
+ error: settlement.outcome === 'error' ? settlement.error.message : closeErrorText(state) });
203
+ return;
204
+ }
205
+ let outcome;
206
+ try {
207
+ outcome = mapSteerOutcome(settlement.result);
208
+ }
209
+ catch (error) {
210
+ out.unjournaled({ t: 'agent_steered', id, ok: false, error: error instanceof Error ? error.message : String(error) });
211
+ return;
212
+ }
213
+ if (outcome === 'injected')
214
+ rememberSteered(state, id);
215
+ out.unjournaled({ t: 'agent_steered', id, ok: true, outcome });
216
+ }
217
+ /** `agent_prompt_failed` for a steer whose blocks were gone: it never reached the agent. */
218
+ export function failUnsentSteer(state, entry, error, out) {
219
+ const { id } = entry.ctx;
220
+ const steer = getOwn(state.steers, id);
221
+ if (!steer || steer.sent)
222
+ return false;
223
+ delete state.outbound[String(entry.id)];
224
+ delete state.steers[id];
225
+ out.unjournaled({ t: 'agent_steered', id, ok: false, error });
226
+ return true;
227
+ }
228
+ /** `agent_opened.steeredIds` (spec §5.5.4): see the file header. */
229
+ export function steeredIds(state) {
230
+ const inFlight = Object.entries(state.steers).filter(([, steer]) => steer.sent).map(([id]) => id);
231
+ return [...state.steered, ...inFlight];
232
+ }
233
+ // ---- background tasks, launch -------------------------------------------------------
234
+ /** `agent_async_task_stop{asyncTaskId}` — `asyncTaskStopRequest`; its answer is diagnostics only. */
235
+ export function onAsyncTaskStopNote(state, note, out) {
236
+ const { asyncTaskId } = note;
237
+ if (typeof asyncTaskId !== 'string' || !asyncTaskId.trim() || sessionReady(state) !== undefined) {
238
+ state.diagnostics.droppedNotes += 1;
239
+ return;
240
+ }
241
+ request(state, out, ASYNC_TASK_STOP_METHOD, { sessionId: state.rootSessionId, asyncTaskId }, 'rpc', undefined);
242
+ }
243
+ /** `agent_launch{launch}` (see `runtime.ts`). */
244
+ export function onLaunch(state, note) {
245
+ if (!onLaunchNote(state, note.launch))
246
+ state.diagnostics.droppedNotes += 1;
247
+ }
248
+ /**
249
+ * `agent_install_outcome{ok, agentName, version, error?}` (Task 20): the adapter install an open had to run first finished within
250
+ * the open's budget (spec §6.2). The daemon writes it right after `agent_launch`; it becomes one past-tense `step` on the session's
251
+ * timeline, numbered like any other event — the words the manager used to journal itself (interface-contracts, 15a/15c).
252
+ */
253
+ export function onInstallOutcomeNote(state, note, out) {
254
+ const { ok, agentName, version, error } = note;
255
+ if (typeof ok !== 'boolean' || typeof agentName !== 'string' || typeof version !== 'string' || (!ok && typeof error !== 'string')) {
256
+ state.diagnostics.droppedNotes += 1;
257
+ return;
258
+ }
259
+ emitEvent(state, out, {
260
+ eventName: 'step',
261
+ payload: ok
262
+ ? { title: 'Agent adapter', message: `Installed ${agentName} adapter ${version}`, tone: 'neutral' }
263
+ : { title: 'Agent adapter', message: `Could not install ${agentName} adapter ${version}`, detail: error, tone: 'error' },
264
+ });
265
+ }
266
+ /**
267
+ * `agent_output_gap{detail}` (Task 21): this session numbers on in a transport other than the one an earlier life of the ref ran in,
268
+ * and that life's unacknowledged output could not be read (keeper-acp unreachable while the session runs in the daemon) — its frames
269
+ * may never reach the server. The daemon seeded the seq above everything that life may have numbered (spec §5.5), so nothing is
270
+ * reused; this past-tense `step` says that output may be missing.
271
+ */
272
+ export function onOutputGapNote(state, note, out) {
273
+ const { detail } = note;
274
+ if (typeof detail !== 'string' || detail.length === 0) {
275
+ state.diagnostics.droppedNotes += 1;
276
+ return;
277
+ }
278
+ emitEvent(state, out, {
279
+ eventName: 'step',
280
+ payload: { title: 'Agent session', message: 'Earlier output of this session could not be recovered', detail, tone: 'error' },
281
+ });
282
+ }
@@ -0,0 +1,26 @@
1
+ import { validateWireEvent } from '../validate.js';
2
+ export function nextSeq(state) {
3
+ state.seq += 1;
4
+ return state.seq;
5
+ }
6
+ /**
7
+ * A journaled `agent_event` (the wire frame minus `sessionRef`, which the projection adds). Returns
8
+ * whether it was emitted.
9
+ */
10
+ export function emitEvent(state, out, event) {
11
+ if (validateWireEvent(event)) {
12
+ state.diagnostics.invalidEvents += 1;
13
+ return false;
14
+ }
15
+ out.frame({
16
+ t: 'agent_event',
17
+ seq: nextSeq(state),
18
+ eventName: event.eventName,
19
+ payload: event.payload,
20
+ ...(event.acpSessionId ? { acpSessionId: event.acpSessionId } : {}),
21
+ });
22
+ return true;
23
+ }
24
+ export function isRecord(value) {
25
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
26
+ }