@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
@@ -0,0 +1,314 @@
1
+ /** Bundled release certification is the trust root; writable receipts never authorize a runtime. */
2
+ import { spawn } from 'node:child_process';
3
+ import { constants } from 'node:fs';
4
+ import { lstat, mkdtemp, open, readFile, readdir, realpath, rm } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import semver from 'semver';
9
+ import { inside, assertSafeAgentId, assertSafePackageName } from './builds/model.js';
10
+ import { nodeVersionOf } from './builds/nodeGate.js';
11
+ import { expectedPatchedFiles, patchDigest, verifyPatchedFiles } from './managedPatches.js';
12
+ import { managedRuntimePolicies, packagePatches, patchAssetRoot, validatePackagePatch, validatePatchPath } from './patchAssets.js';
13
+ export const REQUIRED_RUNTIME_CHECKS = ['initialize', 'ordinary-input', 'permissions', 'images-settings', 'parallel-subagents', 'background-reconnect', 'next-append', 'next-button', 'stop', 'cached-resume', 'legacy-migration', 'explicit-load', 'replay', 'install-gates', 'platform'];
14
+ const hashPattern = /^[a-f0-9]{64}$/;
15
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
16
+ function requireThat(condition, message) {
17
+ if (!condition)
18
+ throw new Error(`Managed runtime release: ${message}`);
19
+ }
20
+ function exactVersion(value) { return typeof value === 'string' && semver.valid(value) === value && semver.prerelease(value) === null; }
21
+ export function managedRuntimeFor(agent, policies = managedRuntimePolicies) {
22
+ const found = policies.filter(policy => policy.agent === agent);
23
+ requireThat(found.length <= 1, 'duplicate agent policy');
24
+ return found[0];
25
+ }
26
+ export function assertRuntimeRequest(policy, name, version) {
27
+ assertSafeAgentId(policy.agent);
28
+ assertSafePackageName(policy.package);
29
+ validatePatchPath(policy.release);
30
+ requireThat(exactVersion(policy.version) && policy.ignore_scripts === true, 'invalid exact version or script policy');
31
+ requireThat(Array.isArray(policy.required_patches) && policy.required_patches.length > 0 && policy.required_patches.every(id => typeof id === 'string' && id.length > 0) && new Set(policy.required_patches).size === policy.required_patches.length, 'invalid required patches');
32
+ requireThat(name === policy.package && version === policy.version, `only certified ${policy.package}@${policy.version} may be installed`);
33
+ }
34
+ /**
35
+ * Reject aliases in every component; use nonblocking nofollow opens so a FIFO cannot hang
36
+ * verification. Root-owned system symlinks (`/var -> /private/var` on macOS, in front of every
37
+ * per-user tmpdir) are crossed, not refused: a root-owned link to a root-owned, non-group-writable
38
+ * directory is not plantable by the running user. Every user-owned symlink is still refused.
39
+ */
40
+ async function ownedFile(filename) {
41
+ const full = path.resolve(filename);
42
+ let current = path.parse(full).root;
43
+ const systemSymlink = async (uid) => {
44
+ if (uid !== 0 || process.getuid?.() === 0)
45
+ return false;
46
+ try {
47
+ const target = await realpath(current);
48
+ const stat = await lstat(target);
49
+ return stat.isDirectory() && stat.uid === 0 && (stat.mode & 0o022) === 0;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ };
55
+ for (const part of full.slice(current.length).split(path.sep).filter(Boolean)) {
56
+ current = path.join(current, part);
57
+ const info = await lstat(current);
58
+ requireThat(!info.isSymbolicLink() || (await systemSymlink(info.uid)), 'symlink in runtime path');
59
+ }
60
+ const file = await open(full, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
61
+ if (!(await file.stat()).isFile()) {
62
+ await file.close();
63
+ throw new Error('Managed runtime requires a regular file');
64
+ }
65
+ return file;
66
+ }
67
+ async function readReleaseAsset(relative, assets) {
68
+ validatePatchPath(relative);
69
+ const base = assets.assetRoot ?? patchAssetRoot;
70
+ const handle = await ownedFile(inside(base instanceof URL ? fileURLToPath(base) : base, relative));
71
+ try {
72
+ requireThat((await handle.stat()).size <= 8 * 1024 * 1024, 'manifest too large');
73
+ return JSON.parse(await handle.readFile('utf8'));
74
+ }
75
+ finally {
76
+ await handle.close();
77
+ }
78
+ }
79
+ export async function loadRuntimeRelease(policy, assets = {}) {
80
+ assertRuntimeRequest(policy, policy.package, policy.version);
81
+ return validateRuntimeRelease(await readReleaseAsset(policy.release, assets), policy, assets);
82
+ }
83
+ function validateRuntimeRelease(r, policy, assets, retained = false) {
84
+ assertRuntimeRequest(policy, policy.package, policy.version);
85
+ requireThat(r?.schema === 1 && typeof r.id === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(r.id), 'invalid schema/id');
86
+ requireThat(r.package === policy.package && r.version === policy.version, 'policy/package/version mismatch');
87
+ requireThat(typeof r.integrity === 'string' && /^sha512-[A-Za-z0-9+/]{86}==$/.test(r.integrity), 'invalid archive integrity');
88
+ requireThat(r.bin && typeof r.bin.name === 'string' && r.bin.name.length > 0, 'invalid bin');
89
+ validatePatchPath(r.bin.path);
90
+ requireThat(r.bin.args === undefined || (Array.isArray(r.bin.args) && r.bin.args.every(arg => typeof arg === 'string' && !arg.includes('\0'))), 'invalid static argv');
91
+ requireThat(r.agentInfo && typeof r.agentInfo.name === 'string' && r.agentInfo.name.length > 0 && exactVersion(r.agentInfo.version), 'invalid agent identity');
92
+ requireThat(r.node && typeof r.node.range === 'string' && semver.validRange(r.node.range), 'invalid Node range');
93
+ requireThat(Array.isArray(r.historyVersions) && r.historyVersions.includes(r.version) && r.historyVersions.every(exactVersion) && new Set(r.historyVersions).size === r.historyVersions.length, 'invalid history versions');
94
+ requireThat(same(r.patches, policy.required_patches), 'ordered required patches mismatch');
95
+ requireThat(r.files && typeof r.files === 'object' && !Array.isArray(r.files) && Object.keys(r.files).length > 0, 'missing trusted files');
96
+ for (const [file, hash] of Object.entries(r.files)) {
97
+ validatePatchPath(file);
98
+ requireThat(typeof hash === 'string' && hashPattern.test(hash), 'invalid trusted file digest');
99
+ }
100
+ requireThat(r.files['package.json'] && r.files[r.bin.path], 'manifest and executable must be trusted files');
101
+ requireThat(r.capabilities?.cachedReconnect === 'resume' && same(r.capabilities.steering, ['next']) && r.capabilities.subagents === true, 'invalid capabilities');
102
+ const c = r.certification;
103
+ requireThat(c && typeof c.suite === 'string' && c.suite.length > 0 && Array.isArray(c.checks) && Array.isArray(c.platforms), 'missing certification');
104
+ requireThat(c.checks.length === REQUIRED_RUNTIME_CHECKS.length && new Set(c.checks.map(v => v.name)).size === c.checks.length && REQUIRED_RUNTIME_CHECKS.every(name => c.checks.some(v => v.name === name)), 'incomplete or duplicate required checks');
105
+ requireThat(c.checks.every(v => ['PASS', 'NOT_PASS'].includes(v.status) && hashPattern.test(v.evidenceSha256)), 'invalid certification check status');
106
+ requireThat(c.platforms.every(v => typeof v.platform === 'string' && typeof v.arch === 'string' && ['PASS', 'NOT_PASS'].includes(v.status) && hashPattern.test(v.evidenceSha256)) && new Set(c.platforms.map(v => `${v.platform}/${v.arch}`)).size === c.platforms.length, 'invalid platform records');
107
+ requireThat(c.platforms.some(v => v.platform === process.platform && v.arch === process.arch), 'platform record missing');
108
+ requireThat(r.runtimeTools === undefined || Array.isArray(r.runtimeTools), 'invalid runtime tools');
109
+ const tools = r.runtimeTools ?? [];
110
+ requireThat(new Set(tools.map(v => `${v.package?.name ?? r.package}/${v.platform}/${v.arch}/${v.path}`)).size === tools.length, 'duplicate runtime tools');
111
+ for (const tool of tools) {
112
+ validatePatchPath(tool.path);
113
+ if (tool.package !== undefined) {
114
+ requireThat(tool.package && typeof tool.package.name === 'string' && typeof tool.package.version === 'string' && semver.valid(tool.package.version) === tool.package.version, 'invalid exact tool package identity');
115
+ assertSafePackageName(tool.package.name);
116
+ }
117
+ const primary = tool.package === undefined || tool.package.name === r.package;
118
+ requireThat(typeof tool.platform === 'string' && typeof tool.arch === 'string' && hashPattern.test(tool.sha256) && (!primary || r.files[tool.path] === tool.sha256) && c.platforms.some(v => v.platform === tool.platform && v.arch === tool.arch), 'tool must match a trusted digest and declared platform');
119
+ }
120
+ if (retained)
121
+ requireThat(typeof r.patchDigest === 'string' && hashPattern.test(r.patchDigest), 'retained release requires trusted patch digest');
122
+ else {
123
+ const digest = patchDigest(runtimeRecipes(policy, r, assets.recipes));
124
+ requireThat(r.patchDigest === undefined || r.patchDigest === digest, 'release patch digest disagrees with bundled recipes');
125
+ }
126
+ return r;
127
+ }
128
+ export function runtimeRecipes(policy, release, recipes = packagePatches) {
129
+ const selected = recipes.filter(recipe => recipe.package === policy.package && recipe.versions?.includes(policy.version));
130
+ requireThat(same(selected.map(v => v.id), policy.required_patches), 'missing or unordered exact patch recipe');
131
+ for (const recipe of selected) {
132
+ validatePackagePatch(recipe);
133
+ requireThat(recipe.bin === undefined || recipe.bin === release.bin.name, 'patch bin disagrees with release');
134
+ }
135
+ for (const [qualified, hash] of Object.entries(expectedPatchedFiles(selected))) {
136
+ requireThat(release.files[qualified.slice(policy.package.length + 1)] === hash, 'patch digest disagrees with trusted release files');
137
+ }
138
+ return selected;
139
+ }
140
+ /** Reverse the declared edits to derive trusted pristine hashes, without trusting package metadata. */
141
+ function pristineFiles(release, recipes) {
142
+ const files = { ...release.files };
143
+ for (const recipe of [...recipes].reverse()) {
144
+ for (const asset of recipe.assets ?? [])
145
+ delete files[asset.target];
146
+ for (const edit of [...recipe.edits].reverse()) {
147
+ requireThat(files[edit.file] === edit.after_sha256 && hashPattern.test(edit.before_sha256), 'inconsistent patch hash chain');
148
+ files[edit.file] = edit.before_sha256;
149
+ }
150
+ }
151
+ return files;
152
+ }
153
+ export async function verifyRuntimeTree(options) {
154
+ const r = await loadRuntimeRelease(options.policy, options);
155
+ return verifyReleaseTree(options, r);
156
+ }
157
+ async function verifyReleaseTree(options, r) {
158
+ const pkg = inside(path.join(options.treeRoot, 'node_modules'), r.package);
159
+ const files = options.pristine ? pristineFiles(r, runtimeRecipes(options.policy, r, options.recipes)) : r.files;
160
+ await verifyPatchedFiles(pkg, files);
161
+ // Extra executable code or links must not survive a cache adoption, even with matching package.json.
162
+ const actual = [];
163
+ const walk = async (dir, prefix = '') => {
164
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
165
+ const rel = prefix + entry.name;
166
+ if (entry.isDirectory())
167
+ await walk(path.join(dir, entry.name), rel + '/');
168
+ else {
169
+ requireThat(entry.isFile(), 'non-regular runtime asset');
170
+ actual.push(rel);
171
+ }
172
+ }
173
+ };
174
+ await walk(pkg);
175
+ requireThat(same(actual.sort(), Object.keys(files).sort()), 'unexpected or missing runtime files');
176
+ const metadata = JSON.parse(await readFile(path.join(pkg, 'package.json'), 'utf8'));
177
+ requireThat(metadata.name === r.package && metadata.version === r.version && metadata.bin?.[r.bin.name] === r.bin.path, 'package identity/bin mismatch');
178
+ for (const tool of r.runtimeTools ?? []) {
179
+ if (tool.platform !== process.platform || tool.arch !== process.arch)
180
+ continue;
181
+ const toolPackage = tool.package ?? { name: r.package, version: r.version };
182
+ const toolDir = inside(path.join(options.treeRoot, 'node_modules'), toolPackage.name);
183
+ const manifest = await ownedFile(path.join(toolDir, 'package.json'));
184
+ try {
185
+ const identity = JSON.parse(await manifest.readFile('utf8'));
186
+ requireThat(identity.name === toolPackage.name && identity.version === toolPackage.version, 'runtime tool package identity mismatch');
187
+ }
188
+ finally {
189
+ await manifest.close();
190
+ }
191
+ // External tools are authenticated by this explicit record, not by a claim about their whole package.
192
+ await verifyPatchedFiles(toolDir, { [tool.path]: tool.sha256 });
193
+ const handle = await ownedFile(inside(toolDir, tool.path));
194
+ try {
195
+ if (options.prepareTools)
196
+ await handle.chmod(0o755);
197
+ requireThat(process.platform === 'win32' || ((await handle.stat()).mode & 0o111) !== 0, `runtime tool not executable: ${tool.path}`);
198
+ }
199
+ finally {
200
+ await handle.close();
201
+ }
202
+ }
203
+ return r;
204
+ }
205
+ export async function verifyManagedRuntime(options) {
206
+ const release = await loadRuntimeRelease(options.policy, options);
207
+ await verifyReleaseBuild(options, release, patchDigest(runtimeRecipes(options.policy, release, options.recipes)));
208
+ }
209
+ /** Explicit recorded bindings/rollback only. Every historical authority is bundled and listed, never a writable receipt. */
210
+ export async function verifyRecordedManagedRuntime(options) {
211
+ const active = await loadRuntimeRelease(options.policy, options);
212
+ if (options.build.runtimeReleaseId === active.id) {
213
+ await verifyReleaseBuild(options, active, patchDigest(runtimeRecipes(options.policy, active, options.recipes)));
214
+ return;
215
+ }
216
+ const paths = options.policy.retained_releases ?? [];
217
+ requireThat(Array.isArray(paths) && new Set(paths).size === paths.length && !paths.includes(options.policy.release), 'duplicate or active retained release path');
218
+ const ids = new Set([active.id]);
219
+ let match;
220
+ for (const relative of paths) {
221
+ const release = await readReleaseAsset(relative, options);
222
+ requireThat(release?.schema === 1 && typeof release.id === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(release.id) && !ids.has(release.id), 'invalid, duplicate or active retained release identity');
223
+ ids.add(release.id);
224
+ if (release.id === options.build.runtimeReleaseId)
225
+ match = { release, path: relative };
226
+ }
227
+ requireThat(match, 'recorded release is not explicitly retained');
228
+ const policy = { agent: options.policy.agent, package: match.release.package, version: match.release.version, release: match.path, required_patches: match.release.patches, ignore_scripts: true };
229
+ const release = validateRuntimeRelease(match.release, policy, options, true);
230
+ requireThat(release.package !== options.policy.package || semver.lte(release.version, options.policy.version), 'retained release is a future version');
231
+ await verifyReleaseBuild({ ...options, policy }, release, release.patchDigest);
232
+ }
233
+ async function verifyReleaseBuild(options, release, digest) {
234
+ const { policy, build } = options;
235
+ requireThat(release.certification.checks.length === REQUIRED_RUNTIME_CHECKS.length && release.certification.checks.every(v => v.status === 'PASS'), 'release certification is NOT_PASS');
236
+ requireThat(release.certification.platforms.some(v => v.platform === process.platform && v.arch === process.arch && v.status === 'PASS'), 'platform is NOT_PASS');
237
+ assertRuntimeRequest(policy, build.package ?? '', build.version);
238
+ await verifyReleaseTree({ ...options, treeRoot: inside(options.storeRoot, build.dir), pristine: false }, release);
239
+ requireThat(build.agentId === policy.agent && build.runtimeReleaseId === release.id, 'build release identity mismatch');
240
+ requireThat(build.kind === 'npm-patched' && build.patchDigest === digest && same(build.patches, release.patches), 'build patch identity mismatch');
241
+ requireThat(build.launch.entry === `node_modules/${release.package}/${release.bin.path}` && build.launch.runtime === 'node', 'build executable/dispatcher mismatch');
242
+ requireThat(same(build.launch.args, release.bin.args ?? []), 'build static argv mismatch');
243
+ const nodeVersion = await nodeVersionOf(options.nodePath);
244
+ requireThat(semver.satisfies(nodeVersion, release.node.range, { includePrerelease: false }), `Node ${nodeVersion} outside ${release.node.range}`);
245
+ if (options.initialize)
246
+ await initializeProbe(build, options.storeRoot, options.nodePath, release);
247
+ }
248
+ /** One initialize only, no credentials or user HOME, bounded streams/time, awaited process cleanup. */
249
+ async function initializeProbe(build, storeRoot, nodePath, release) {
250
+ const home = await mkdtemp(path.join(tmpdir(), 'runtime-probe-'));
251
+ try {
252
+ await new Promise((resolve, reject) => {
253
+ const dir = inside(storeRoot, build.dir);
254
+ const child = spawn(nodePath, [inside(dir, build.launch.entry), ...build.launch.args], {
255
+ cwd: home, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'],
256
+ env: { PATH: [path.dirname(nodePath), ...(build.launch.pathDirs ?? []).map(p => inside(dir, p)), process.env.PATH ?? ''].join(path.delimiter), HOME: home, USERPROFILE: home, APPDATA: home, LOCALAPPDATA: home, XDG_CONFIG_HOME: home, XDG_CACHE_HOME: home, TMPDIR: home, TEMP: home, TMP: home, ...(process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {}) },
257
+ });
258
+ let output = '';
259
+ let bytes = 0;
260
+ let stderrBytes = 0;
261
+ let response = false;
262
+ let failure;
263
+ const fail = (message) => { failure ??= new Error(`Managed runtime initialize: ${message}`); child.kill('SIGKILL'); };
264
+ const timeout = setTimeout(() => fail('timed out'), 30_000);
265
+ child.on('error', error => { failure ??= error; });
266
+ child.stdin.on('error', () => { });
267
+ child.stderr.on('data', (data) => { stderrBytes += data.length; if (stderrBytes > 1024 * 1024)
268
+ fail('stderr limit exceeded'); });
269
+ child.stdout.on('data', (data) => {
270
+ bytes += data.length;
271
+ if (bytes > 1024 * 1024) {
272
+ fail('stdout limit exceeded');
273
+ return;
274
+ }
275
+ output += data.toString('utf8');
276
+ let end;
277
+ while ((end = output.indexOf('\n')) >= 0) {
278
+ const line = output.slice(0, end);
279
+ output = output.slice(end + 1);
280
+ try {
281
+ requireThat(!response, 'extra initialize response');
282
+ const msg = JSON.parse(line);
283
+ const result = msg.result;
284
+ requireThat(msg.jsonrpc === '2.0' && msg.id === 'axom-runtime-initialize' && !Object.hasOwn(msg, 'error') && !Object.hasOwn(msg, 'method') && result?.protocolVersion === 1, 'invalid correlated initialize result');
285
+ requireThat(result.agentInfo?.name === release.agentInfo.name && result.agentInfo?.version === release.agentInfo.version, 'initialize agent identity mismatch');
286
+ requireThat(result._meta?.axom?.releaseId === release.id && result._meta?.axom?.cachedReconnect === 'resume', 'initialize release/resume mismatch');
287
+ requireThat(result.agentCapabilities?.loadSession === true && result.agentCapabilities?.sessionCapabilities?.resume && typeof result.agentCapabilities.sessionCapabilities.resume === 'object' && !Array.isArray(result.agentCapabilities.sessionCapabilities.resume), 'missing resume capability');
288
+ requireThat(result._meta?.steering?.supported === true && same(result._meta.steering.timings, ['next']), 'missing next capability');
289
+ response = true;
290
+ child.stdin.end();
291
+ // Await graceful EOF cleanup within the same overall deadline. Native agents may
292
+ // flush telemetry before exiting; a second, shorter deadline can kill a healthy one.
293
+ }
294
+ catch (error) {
295
+ fail(error.message);
296
+ }
297
+ }
298
+ });
299
+ child.once('close', (code) => {
300
+ clearTimeout(timeout);
301
+ if (!failure && (!response || output.trim() || code !== 0))
302
+ failure = new Error('Managed runtime initialize: incomplete response or startup failure');
303
+ if (failure)
304
+ reject(failure);
305
+ else
306
+ resolve();
307
+ });
308
+ child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: 'axom-runtime-initialize', method: 'initialize', params: { protocolVersion: 1, clientInfo: { name: 'axom-runtime-check', version: '1.0.0' }, clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, terminal: true } } }) + '\n');
309
+ });
310
+ }
311
+ finally {
312
+ await rm(home, { recursive: true, force: true });
313
+ }
314
+ }
@@ -0,0 +1,330 @@
1
+ /**
2
+ * ## Subagent relations bypass the window (fix round 1, spec §5.6)
3
+ *
4
+ * The server holds back its applied watermark — so its acks — at the first child frame whose subagent relation (`subagent_spawned`,
5
+ * the server's `TimelineEvent.SubagentSpawned`) it has not received yet (`noteApplied` capped at `nativePendingBefore - 1`,
6
+ * `ExternalAgentEventApplier.kt`). Every later frame then counts against the window; if the relation comes 256 or more frames after
7
+ * that first orphan child frame, a window that held it back would stall the session for good (the server waits 30 s, poisons, the
8
+ * reopen repeats it). So:
9
+ *
10
+ * - **producing**: while a child frame whose relation is not recorded yet is unacknowledged ({@link orphanOutstanding}), the record
11
+ * loop may run past the window, so the relation can be folded at all. Whatever the branch, this life never holds more than
12
+ * {@link RELATION_WAIT_EVENTS} / {@link RELATION_WAIT_BYTES} (1024 frames / 32 MiB) unacknowledged — pinned entries included — plus
13
+ * the frames of the one record being folded: that is the daemon's memory bound per session;
14
+ * - **sending**: once a relation that resolves such a child (a *resolving* relation) is recorded, it and every entry before it stop
15
+ * counting against either window ({@link pinnedThrough}): the sender takes them past the window, in seq order (sending the
16
+ * relation first would make the server drop the lower seqs as duplicates); the socket gate still applies. They must keep not
17
+ * counting after they are sent: the server applied them but wasted its acks while capped (each ack attempt resets its 32-event
18
+ * count), so its next ack needs up to 31 more applied frames — a window still full of them would deadlock there. They leave the
19
+ * outbox with that ack. A relation that resolves nothing recorded is not what the server waits for and keeps to the window
20
+ * (sending past it could overflow a parked server's 512-frame hold-off).
21
+ *
22
+ * ## Residual (documented, not fixed)
23
+ *
24
+ * While the producing window is full — a server parked on an approval with ≥ 256 root frames held, a connection that is down, an
25
+ * applier that never acknowledges — the record loop folds nothing: a steer or config RPC waits for the processor's reply and can
26
+ * time out on the server; an `agent_interrupt` note is folded (and the `session/cancel` written) only once the server unparks.
27
+ */
28
+ /** Unacknowledged events per session (spec §5.6, `W_EVT`). `ACK_EVERY_EVENTS(32) < W_EVT ≤ HOLD_OFF_CAPACITY(512)`. */
29
+ export const SESSION_WINDOW_EVENTS = 256;
30
+ /** Unacknowledged bytes per session (spec §5.6): a few huge frames cannot hog the window. */
31
+ export const SESSION_WINDOW_BYTES = 8 * 1024 * 1024;
32
+ /** How far production may run past the window while a recorded child frame still waits for its relation (bounds memory). */
33
+ export const RELATION_WAIT_EVENTS = 4 * SESSION_WINDOW_EVENTS;
34
+ export const RELATION_WAIT_BYTES = 4 * SESSION_WINDOW_BYTES;
35
+ /** A frame carrying a subagent relation (the server's `SubagentSpawned`): it bypasses the session window. */
36
+ export function isRelationFrame(frame) {
37
+ return frame.t === 'agent_event' && frame.eventName === 'subagent_spawned';
38
+ }
39
+ /**
40
+ * `src.i` of an outbox entry no record produced: a frame this daemon numbered itself (a degraded session's failed `turn_done`,
41
+ * `src.o` = the last record folded before it) or one imported from a legacy journal (`src.o` = 0). A replay never reproduces it
42
+ * (T21 skips it when comparing). Far above any per-record frame index.
43
+ */
44
+ export const DAEMON_FRAME_INDEX = 1 << 24;
45
+ /**
46
+ * Connection epoch of an entry that may have reached the server on an earlier connection (or in an earlier daemon life): it is not
47
+ * sent again until the server reopens the ref and says how far it got ({@link SessionOutbox.resendAll} after an `ack` of the open's
48
+ * `resumeFromSeq`) — resending a frame the server already applied could, for a `turn_done`, end a newer turn (analysis B §3.1).
49
+ */
50
+ export const SENT_EARLIER = -1;
51
+ const frameBytes = (frame) => Buffer.byteLength(JSON.stringify(frame));
52
+ export function isOutboxTail(value) {
53
+ if (typeof value !== 'object' || value === null)
54
+ return false;
55
+ const { seq, src } = value;
56
+ return Number.isSafeInteger(seq) && typeof src === 'object' && src !== null &&
57
+ Number.isSafeInteger(src.o) && Number.isSafeInteger(src.i);
58
+ }
59
+ export class SessionOutbox {
60
+ store;
61
+ items = [];
62
+ committed;
63
+ highestSeq = 0;
64
+ ownEvents = 0;
65
+ ownBytes = 0;
66
+ chain = Promise.resolve();
67
+ roomWaiters = new Set();
68
+ listeners = new Set();
69
+ stopped = false;
70
+ /** Children whose relation frame this outbox recorded. */
71
+ related = new Set();
72
+ /** Seq of the latest resolving relation: entries at or below it count against neither window (file comment). */
73
+ pinned = 0;
74
+ isRelated;
75
+ /**
76
+ * `isRelated`: whether the session already knows a child's relation from elsewhere (the processor's relation graph — a relation
77
+ * recorded by an earlier life, or replayed by a load). Without it only relations this outbox recorded count.
78
+ */
79
+ constructor(store, init = {}) {
80
+ this.store = store;
81
+ this.isRelated = init.isRelated ?? (() => false);
82
+ this.committed = isOutboxTail(init.committedTail) ? { seq: init.committedTail.seq, src: { ...init.committedTail.src } } : null;
83
+ this.highestSeq = this.committed?.seq ?? 0;
84
+ for (const record of init.leftovers ?? []) {
85
+ // Read back past the watermark only; a record at or below it is acknowledged (truncation is by whole segments).
86
+ if (this.committed && record.seq <= this.committed.seq)
87
+ continue;
88
+ this.items.push({ o: record.o, seq: record.seq, src: { ...record.src }, frame: record.frame, bytes: frameBytes(record.frame), leftover: true,
89
+ ...this.classify(record.seq, record.frame), sentOn: SENT_EARLIER });
90
+ this.highestSeq = Math.max(this.highestSeq, record.seq);
91
+ }
92
+ }
93
+ /**
94
+ * Resolving relation / orphan frame, in record order (a relation relates its child for every later frame). An orphan is a frame
95
+ * the server retains until a relation it has not received: a child frame (`acpSessionId`) whose child is unrelated, and — Task
96
+ * 22 — a `subagent_state_update` (any origin) naming an unrelated child (`payload.subagentSessionId`): the server's
97
+ * `receiveNativeEventLocked` holds it, and with it its acks, until that child's `subagent_spawned` exactly as it holds a child
98
+ * frame. The state update's own child is the key: a root-origin one would otherwise not be an orphan at all, its relation would
99
+ * keep to the window, and the session would stall where Task 20 fixed it for child frames.
100
+ */
101
+ classify(seq, frame) {
102
+ const unrelated = (child) => child !== undefined && !this.related.has(child) && !this.isRelated(child) ? child : undefined;
103
+ const origin = frame.t === 'agent_event' && typeof frame.acpSessionId === 'string' ? frame.acpSessionId : undefined;
104
+ const stateChild = frame.t === 'agent_event' && frame.eventName === 'subagent_state_update'
105
+ ? frame.payload?.subagentSessionId
106
+ : undefined;
107
+ const orphanOf = unrelated(typeof stateChild === 'string' ? stateChild : undefined) ?? unrelated(origin);
108
+ let resolving = false;
109
+ if (isRelationFrame(frame)) {
110
+ const child = frame.payload?.subagentSessionId;
111
+ if (typeof child === 'string') {
112
+ resolving = this.items.some((item) => item.orphanOf === child);
113
+ this.related.add(child);
114
+ if (resolving)
115
+ this.pinned = Math.max(this.pinned, seq);
116
+ }
117
+ }
118
+ return { resolving, ...(orphanOf !== undefined ? { orphanOf } : {}) };
119
+ }
120
+ /** Entries at or below this seq count against neither window (the latest resolving relation; 0: none). */
121
+ get pinnedThrough() { return this.pinned; }
122
+ /** An unacknowledged child frame whose relation is still not recorded: the server is (likely) holding its acks for it. */
123
+ orphanOutstanding() {
124
+ return this.items.some((item) => item.orphanOf !== undefined && !this.related.has(item.orphanOf) && !this.isRelated(item.orphanOf));
125
+ }
126
+ /** Highest seq this outbox ever held (entries, or the acknowledged watermark): a new life numbers after it (§5.5). */
127
+ get lastSeq() { return this.highestSeq; }
128
+ /** The acknowledged watermark (`committedTail`), `null` before the first truncating ack. */
129
+ get committedTail() { return this.committed; }
130
+ /** Unacknowledged entries, in seq order. */
131
+ get pending() { return this.items; }
132
+ /** Unacknowledged entries appended by this life (the producing window), past {@link pinnedThrough}. */
133
+ get backlog() {
134
+ if (this.pinned === 0)
135
+ return { events: this.ownEvents, bytes: this.ownBytes };
136
+ let events = 0;
137
+ let bytes = 0;
138
+ for (const item of this.items)
139
+ if (!item.leftover && item.seq > this.pinned) {
140
+ events += 1;
141
+ bytes += item.bytes;
142
+ }
143
+ return { events, bytes };
144
+ }
145
+ /**
146
+ * Entries sent on connection `epoch` and not acknowledged (the sending window). Entries at or below {@link pinnedThrough} are
147
+ * sent past the window either way (the sender bypasses it for them); once sent they count only when `pinnedCountsWhenSent` —
148
+ * a server that acknowledges as soon as the relation releases its cap (`AGENT_WIRE_RELATION_ACK_VERSION`, Task 22). An older
149
+ * server wastes its acks while capped, so its first ack after the relation can need up to 31 more applied frames: counting the
150
+ * pinned ones there would deadlock right after the relation (fix round 1 of Task 20).
151
+ */
152
+ inflight(epoch, pinnedCountsWhenSent = false) {
153
+ let events = 0;
154
+ let bytes = 0;
155
+ for (const item of this.items) {
156
+ if (item.sentOn === epoch && (pinnedCountsWhenSent || item.seq > this.pinned)) {
157
+ events += 1;
158
+ bytes += item.bytes;
159
+ }
160
+ }
161
+ return { events, bytes };
162
+ }
163
+ /** Called whenever there may be something new to send (an append, an ack opening the window, a resend). */
164
+ onChange(listener) {
165
+ this.listeners.add(listener);
166
+ return () => { this.listeners.delete(listener); };
167
+ }
168
+ changed() { for (const listener of [...this.listeners])
169
+ listener(); }
170
+ run(op) {
171
+ const result = this.chain.then(op);
172
+ this.chain = result.then(() => undefined, () => undefined);
173
+ return result;
174
+ }
175
+ /** Waits for every stream operation queued so far. */
176
+ flush() { return this.chain; }
177
+ /**
178
+ * Record frames before they are sent. Resolves once they are durable; rejects when the stream refused them (the caller degrades
179
+ * the session — a frame that could not be recorded is not sent either).
180
+ */
181
+ append(frames) {
182
+ if (frames.length === 0)
183
+ return this.chain;
184
+ return this.run(async () => {
185
+ const entries = frames.map((f) => ({ seq: f.seq, src: { o: f.src.o, i: f.src.i }, frame: { ...f.frame } }));
186
+ const { first } = await this.store.append(entries);
187
+ entries.forEach((entry, index) => {
188
+ const bytes = frameBytes(entry.frame);
189
+ this.items.push({ o: first + index, seq: entry.seq, src: entry.src, frame: entry.frame, bytes, leftover: false, ...this.classify(entry.seq, entry.frame), sentOn: null });
190
+ this.highestSeq = Math.max(this.highestSeq, entry.seq);
191
+ this.ownEvents += 1;
192
+ this.ownBytes += bytes;
193
+ });
194
+ this.wakeRoom(); // a resolving relation unpins the producing window
195
+ this.changed();
196
+ });
197
+ }
198
+ /**
199
+ * The server applied everything through `throughSeq` (`agent_event_ack`, or an open's `resumeFromSeq`): forget those entries and
200
+ * truncate the stream through the last of them, persisting `{seq, src}` of that entry as the watermark. Nothing held at or below
201
+ * it: nothing changes. A failed commit is reported (`onError`) — the entries are acknowledged either way; the stream keeps them
202
+ * until a later commit passes them.
203
+ */
204
+ ack(throughSeq, onError) {
205
+ return this.run(async () => {
206
+ let k = -1;
207
+ for (let i = 0; i < this.items.length && this.items[i].seq <= throughSeq; i++)
208
+ k = i;
209
+ if (k < 0)
210
+ return;
211
+ const last = this.items[k];
212
+ const removed = this.items.splice(0, k + 1);
213
+ for (const item of removed)
214
+ if (!item.leftover) {
215
+ this.ownEvents -= 1;
216
+ this.ownBytes -= item.bytes;
217
+ }
218
+ this.committed = { seq: last.seq, src: { ...last.src } };
219
+ this.wakeRoom();
220
+ this.changed();
221
+ try {
222
+ await this.store.commit({ outbox: { through: last.o, tail: { seq: last.seq, src: { ...last.src } } } });
223
+ }
224
+ catch (error) {
225
+ onError?.(error);
226
+ }
227
+ });
228
+ }
229
+ /**
230
+ * A processor checkpoint at raw `offset`, and raw truncation through it (the keeper refuses anything past it) unless `truncate` is
231
+ * false (a marker checkpoint, Task 21).
232
+ */
233
+ commitCheckpoint(offset, blob, truncate = true) {
234
+ return this.run(async () => {
235
+ await this.store.commit({ checkpoint: { offset, blob }, ...(truncate ? { rawThrough: offset } : {}) });
236
+ });
237
+ }
238
+ /**
239
+ * The session re-attached to keeper-acp after its connection was lost (Task 21): the stream is the truth again. An append whose
240
+ * reply was lost may have landed (it is in `leftovers` now) or not (it is gone, and the record loop folds its record again). The
241
+ * entries are replaced by what the stream holds past its watermark; an entry this object already had keeps whether (and on which
242
+ * connection) it was sent and whether an earlier life left it; one it never saw was never sent. Call it with no stream operation
243
+ * in flight ({@link flush} first). Listeners and room waiters are kept.
244
+ */
245
+ rebase(init) {
246
+ const previous = new Map(this.items.map((item) => [item.seq, item]));
247
+ this.items.length = 0;
248
+ this.related.clear();
249
+ this.pinned = 0;
250
+ this.ownEvents = 0;
251
+ this.ownBytes = 0;
252
+ if (isOutboxTail(init.committedTail) && (this.committed === null || init.committedTail.seq >= this.committed.seq)) {
253
+ this.committed = { seq: init.committedTail.seq, src: { ...init.committedTail.src } };
254
+ }
255
+ this.highestSeq = Math.max(this.highestSeq, this.committed?.seq ?? 0);
256
+ for (const record of init.leftovers) {
257
+ if (this.committed && record.seq <= this.committed.seq)
258
+ continue;
259
+ const before = previous.get(record.seq);
260
+ const leftover = before?.leftover ?? false;
261
+ const bytes = frameBytes(record.frame);
262
+ this.items.push({ o: record.o, seq: record.seq, src: { ...record.src }, frame: record.frame, bytes, leftover,
263
+ ...this.classify(record.seq, record.frame), sentOn: before ? before.sentOn : null });
264
+ if (!leftover) {
265
+ this.ownEvents += 1;
266
+ this.ownBytes += bytes;
267
+ }
268
+ this.highestSeq = Math.max(this.highestSeq, record.seq);
269
+ }
270
+ this.wakeRoom();
271
+ this.changed();
272
+ }
273
+ /** Mark every unacknowledged entry unsent: the sender resends them in order (a reopen's replay, after the ack of its watermark). */
274
+ resendAll() {
275
+ for (const item of this.items)
276
+ item.sentOn = null;
277
+ this.changed();
278
+ }
279
+ /**
280
+ * A new connection: everything recorded so far — sent on an earlier connection or produced while none was bound — is held until
281
+ * the server reopens the ref and says how far it got ({@link SENT_EARLIER}); what is recorded from now on is sent at once. (The
282
+ * journal's semantics: frames produced while unbound were replayed by the reopen, never sent on their own.)
283
+ */
284
+ connectionChanged() {
285
+ for (const item of this.items)
286
+ item.sentOn = SENT_EARLIER;
287
+ }
288
+ /** The first entry never sent (the sender's next candidate), in seq order. */
289
+ nextUnsent() {
290
+ return this.items.find((item) => item.sentOn === null);
291
+ }
292
+ /** Resolves when this life's unacknowledged backlog is below the window (immediately once {@link stopWaiting} ran). */
293
+ room() {
294
+ if (this.hasRoom())
295
+ return undefined;
296
+ return new Promise((resolve) => { this.roomWaiters.add(resolve); });
297
+ }
298
+ hasRoom() {
299
+ if (this.stopped)
300
+ return true;
301
+ // A hard bound on everything this life holds unacknowledged, pinned or not: chained resolving relations each reset `backlog`,
302
+ // but pinned entries leave memory only with an ack.
303
+ if (this.ownEvents >= RELATION_WAIT_EVENTS || this.ownBytes >= RELATION_WAIT_BYTES)
304
+ return false;
305
+ const { events, bytes } = this.backlog;
306
+ if (events < SESSION_WINDOW_EVENTS && bytes < SESSION_WINDOW_BYTES)
307
+ return true;
308
+ // Past the window only while a relation is awaited: the server cannot acknowledge before it, so it must be folded.
309
+ return this.orphanOutstanding();
310
+ }
311
+ wakeRoom() {
312
+ if (!this.hasRoom())
313
+ return;
314
+ for (const resolve of [...this.roomWaiters])
315
+ resolve();
316
+ this.roomWaiters.clear();
317
+ }
318
+ /** The session is closing or degraded: nobody waits for room any more. */
319
+ stopWaiting() {
320
+ this.stopped = true;
321
+ this.wakeRoom();
322
+ }
323
+ }
324
+ /** An outbox store over a transport session (`acp.outbox.append` / `acp.commit`). */
325
+ export function transportOutboxStore(transport, sessionKey) {
326
+ return {
327
+ append: (entries) => transport.appendOutbox(sessionKey, entries),
328
+ commit: (params) => transport.commit(sessionKey, params),
329
+ };
330
+ }