@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,7 +1,13 @@
1
+ import { claimDaemonIdentity } from './control/daemonIdentity.js';
2
+ import { openRuntimeHost } from './control/runtimeHost.js';
3
+ import { stoppedRuntime } from './control/protocol.js';
4
+ import { normalizeWorkstationServer } from './control/serverIdentity.js';
5
+ import { promises as fs } from 'node:fs';
6
+ import os from 'node:os';
1
7
  import path from 'node:path';
2
8
  import { fileURLToPath } from 'node:url';
3
9
  import { AGENT_WIRE_MIN_VERSION } from './protocol/agent.js';
4
- import { clientDir, loadClientConfig, readKeys, updateServiceContext } from './config.js';
10
+ import { clientDir, loadClientConfig, updateServiceContext } from './config.js';
5
11
  import { AgentSessionManager } from './agentSessions.js';
6
12
  import { AcpAgents } from './acp/acpAgents.js';
7
13
  import { ConnectionSupervisor } from './connection/supervisor.js';
@@ -9,10 +15,26 @@ import { ManagementChannel } from './channels/management.js';
9
15
  import { ComputerChannel } from './channels/computer.js';
10
16
  import { AgentChannel, dispatchAgentFrame as dispatchAgentChannelFrame } from './channels/agent.js';
11
17
  import { TaskRegistry } from './update/taskRegistry.js';
18
+ import { RdUpkeep } from './rd/upkeep.js';
12
19
  import { createUpdateCoordinator } from './update/runUpdate.js';
13
20
  import { resolveCliPath } from './daemonctl.js';
14
21
  import { SystemMetricsSampler } from './systemMetrics.js';
15
- import { preflightOptionalPty } from './update/ptyPreflight.js';
22
+ import { cachedPtyStatus, preflightOptionalPty } from './update/ptyPreflight.js';
23
+ import { FilesHostClient } from './files/client.js';
24
+ import { ToolsClient } from './tools/client.js';
25
+ import { WorkstationTools } from './tools/router.js';
26
+ import { aggregateToolsInfo, computeFilesInfo } from './tools/info.js';
27
+ import { defaultDistDir, KeeperLauncher } from './keepers/kit/launcher.js';
28
+ import { readKeeperManifest } from './keepers/kit/bundle.js';
29
+ import { KEEPER_POLICY_TICK_MS, KeeperPolicy, oneShotDrain, oneShotProbe } from './keepers/policy.js';
30
+ import { TerminalGenerations } from './keepers/terminal/generations.js';
31
+ import { AcpKeeperClient } from './keepers/acp/client.js';
32
+ import { KeeperTransport } from './acp/processor/transport/keeper.js';
33
+ import { keeperGenerations } from './acp/processor/transport/generations.js';
34
+ import { ExecPolicy } from './exec/policy.js';
35
+ import { LegacyToolsKeeper } from './keepers/legacy.js';
36
+ import { TerminalRouter, terminalEnv, terminalShell } from './tools/terminalRouter.js';
37
+ import { ToolchainProbe } from './toolchain.js';
16
38
  /**
17
39
  * The path `runDaemon` hands `createUpdateCoordinator` as `cliPath`, used to re-provision the OS
18
40
  * service after an update (see runUpdate.ts's `provisionService`).
@@ -106,6 +128,45 @@ export const IDLE_SWEEP_INTERVAL_MS = 10 * 60 * 1000;
106
128
  * nobody awaits a timer callback, and an escaping rejection is an unhandled rejection, which Node
107
129
  * answers by exiting.
108
130
  */
131
+ /**
132
+ * The daemon's start-time adapter build upkeep: legacy migration, then the certified reconcile; calls
133
+ * `onRosterChanged` once if either changed any agent's current build. Never rejects.
134
+ */
135
+ export async function maintainAdapterBuilds(acp, onRosterChanged) {
136
+ try {
137
+ const migrated = await acp.migrateLegacyAdapters();
138
+ const moves = await acp.reconcileCertifiedBuilds();
139
+ if (migrated.length || moves.length)
140
+ await onRosterChanged();
141
+ }
142
+ catch (error) {
143
+ console.warn(`[acp-builds] adapter build upkeep failed: ${String(error)}`);
144
+ }
145
+ }
146
+ /** How often held sessions are looked at for a pending switch (spec §6.3 step 7) or a lost adapter to reopen (Task 24). */
147
+ export const SWITCH_POLL_INTERVAL_MS = 5_000;
148
+ /** How often unused adapter builds are collected (spec §6.3 "GC"; the first run is one grace period after start). */
149
+ export const BUILD_GC_INTERVAL_MS = 10 * 60_000;
150
+ /**
151
+ * Task 24: the switch scheduler (every {@link SWITCH_POLL_INTERVAL_MS}: `agent_reopen_request` for idle sessions with a pending
152
+ * switch, and for keeper-hosted sessions whose adapter keeper-acp lost) and build GC (every {@link BUILD_GC_INTERVAL_MS}). Both
153
+ * timers are `unref`'d, and neither callback can reject. Returns the stop.
154
+ */
155
+ export function startSwitchUpkeep(agents, acp, pollMs = SWITCH_POLL_INTERVAL_MS, gcMs = BUILD_GC_INTERVAL_MS) {
156
+ let polling = false;
157
+ const poll = setInterval(() => {
158
+ if (polling)
159
+ return;
160
+ polling = true;
161
+ void Promise.resolve().then(() => agents.pollSwitches()).catch((err) => {
162
+ console.error(`[agent] switch poll failed: ${err instanceof Error ? err.message : String(err)}`);
163
+ }).finally(() => { polling = false; });
164
+ }, pollMs);
165
+ poll.unref();
166
+ const gc = setInterval(() => { void Promise.resolve().then(() => acp.collectBuilds()).catch(() => undefined); }, gcMs);
167
+ gc.unref();
168
+ return () => { clearInterval(poll); clearInterval(gc); };
169
+ }
109
170
  export function startIdleSweep(agents, ttlMs = IDLE_SESSION_TTL_MS, intervalMs = IDLE_SWEEP_INTERVAL_MS) {
110
171
  const timer = setInterval(() => {
111
172
  void (async () => {
@@ -157,7 +218,99 @@ export function resolveServiceLifecycle(options) {
157
218
  export async function buildManagementSnapshot(supervisor, tasks, updates) {
158
219
  return { channels: supervisor.channelSnapshot(), ...tasks.snapshot(), update: await updates.snapshotAsync() };
159
220
  }
221
+ /**
222
+ * The files host's configured roots: every workspace cwd, absolute (a relative one is taken relative to the home
223
+ * directory; FileTools refuses relative roots because they would resolve against whatever directory a pinned section
224
+ * holds).
225
+ */
226
+ export function workspaceRoots(cfg, home = os.homedir()) {
227
+ return [path.join(home, '.axom', 'playground'), ...(cfg.workspaces ?? []).filter(workspace => typeof workspace?.cwd === 'string' && workspace.cwd.length > 0).map(workspace => path.resolve(home, workspace.cwd))];
228
+ }
229
+ /** The roots that exist as directories right now. A missing workspace must not keep the host (all of Files) down. */
230
+ export async function existingDirectories(roots) {
231
+ const existing = [];
232
+ for (const root of roots) {
233
+ try {
234
+ if ((await fs.stat(root)).isDirectory())
235
+ existing.push(root);
236
+ }
237
+ catch { /* left out until it exists */ }
238
+ }
239
+ return existing;
240
+ }
241
+ /** How often the daemon re-reads the workspaces config (and root existence) to restart the files host on a change. */
242
+ export const FILES_ROOTS_POLL_MS = 10_000;
243
+ /** How often the legacy tools keeper is re-probed and checked for retirement (keepers/legacy.ts). */
244
+ export const LEGACY_TICK_MS = 30_000;
245
+ /**
246
+ * Builds the object `ManagementChannelOptions.keepers` reports (Task 26): the policy's statuses, never a rejection (a failure is
247
+ * `null`, which the channel also guarantees). `countedExecs` (final review minor 7): keeper-terminal execs `activeShellTasks` also
248
+ * counts right now — reported as the current terminal keeper's `execCounted` metric (sampled with this heartbeat), so the
249
+ * Workstations page counts `activeShellTasks + execRunning − execCounted` commands, each once.
250
+ */
251
+ export async function buildKeeperStatuses(policy, countedExecs = () => 0) {
252
+ let statuses;
253
+ try {
254
+ statuses = await policy.statuses();
255
+ }
256
+ catch {
257
+ return null;
258
+ }
259
+ const terminals = statuses.filter((status) => status.kind === 'terminal' && !status.legacy);
260
+ const target = terminals.find((status) => status.replacement === 'none') ?? terminals[0];
261
+ if (!target)
262
+ return statuses;
263
+ let counted = 0;
264
+ try {
265
+ const value = countedExecs();
266
+ counted = Number.isSafeInteger(value) && value > 0 ? value : 0;
267
+ }
268
+ catch {
269
+ counted = 0;
270
+ }
271
+ // A copy: the policy reuses its sample for a while.
272
+ return statuses.map((status) => (status === target ? { ...status, metrics: { ...status.metrics, execCounted: counted } } : status));
273
+ }
274
+ /**
275
+ * Runs `poll` every `intervalMs`, skipping a tick while the previous run is still in flight (on a hung mount a stat
276
+ * may never return; overlapping runs would pile up behind it). The timer is unref'd. Returns the stop function.
277
+ */
278
+ export function startExclusivePoll(poll, intervalMs) {
279
+ let running = false;
280
+ const timer = setInterval(() => {
281
+ if (running)
282
+ return;
283
+ running = true;
284
+ void poll().catch(() => { }).finally(() => { running = false; });
285
+ }, intervalMs);
286
+ timer.unref();
287
+ return () => clearInterval(timer);
288
+ }
160
289
  export async function runDaemon(options = {}) {
290
+ const directory = clientDir();
291
+ const shutdown = new AbortController();
292
+ const state = { snapshot: async () => ({ ...stoppedRuntime(), connection: 'connecting' }) };
293
+ const { host, identity } = await claimDaemonIdentity(directory, async (identity) => {
294
+ const config = identity.config;
295
+ const host = await openRuntimeHost({ directory, binding: { server: normalizeWorkstationServer(config.server), workstationId: config.workstationId, ...(config.ownerUserId ? { ownerUserId: config.ownerUserId } : {}) },
296
+ mode: options.supervisedByServiceManager ? 'launchd' : 'foreground', snapshot: () => state.snapshot(), shutdown: async () => { shutdown.abort(); },
297
+ update: async (release, operationId) => {
298
+ if (!state.update)
299
+ return { phase: 'BUSY', commandId: operationId };
300
+ void state.update.handle({ commandId: operationId, release, force: false }).catch(error => console.error('[update] local request failed', error));
301
+ return { phase: 'REQUESTED', commandId: operationId, version: release.version };
302
+ },
303
+ });
304
+ return { host, identity };
305
+ });
306
+ try {
307
+ await runOwnedDaemon(options, shutdown, state, identity);
308
+ }
309
+ finally {
310
+ await host.close();
311
+ }
312
+ }
313
+ async function runOwnedDaemon(options, shutdown, localState, identity) {
161
314
  // Last line of defence for the whole process. Node's default for an unhandled rejection is to
162
315
  // *exit*, and this daemon is the only thing keeping every session on this workstation alive —
163
316
  // dying because one async callback rejected is strictly worse than logging and carrying on.
@@ -167,10 +320,27 @@ export async function runDaemon(options = {}) {
167
320
  console.error(`[daemon] unhandled rejection (ignored, daemon stays up): ${detail}`);
168
321
  });
169
322
  const dir = clientDir();
170
- const cfg = await loadClientConfig(dir);
171
- const { clientKey, serverPub } = await readKeys(dir);
323
+ const cfg = identity.config;
324
+ let activateRdAfterReadiness;
325
+ if (process.platform === 'linux') {
326
+ const { synchronizeRd } = await import('./rd/update.js');
327
+ activateRdAfterReadiness = await synchronizeRd(dir, cfg.server);
328
+ }
329
+ const { NativeProvider } = await import('./desktopControl/nativeProvider.js');
330
+ const { DeviceSockets } = await import('./desktopControl/deviceSockets.js');
331
+ const { DesktopSessionSupervisor } = await import('./desktopControl/supervisor.js');
332
+ const { MacProvider } = await import('./desktopControl/macProvider.js');
333
+ const { LinuxDesktopProvider } = await import('./rd/provider.js');
334
+ const legacyDesktop = cfg.desktopProvider ? new NativeProvider({ ...cfg.desktopProvider, validateSession: async () => {
335
+ const { readRdState } = await import('./rd/state.js');
336
+ if (await readRdState(dir))
337
+ throw new Error('Remote desktop configuration changed');
338
+ } }) : undefined;
339
+ const desktopProvider = options.desktopProvider ?? (process.platform === 'linux' ? new LinuxDesktopProvider(dir, cfg.server, legacyDesktop) :
340
+ legacyDesktop ?? (process.platform === 'darwin' && process.arch === 'arm64' ? new MacProvider(dir, cfg) : undefined));
341
+ const { clientKey, serverPub } = identity;
172
342
  // Repair the first fixed release even when it was installed by an older ignore-scripts updater,
173
- // before a newly started terminal keeper snapshots native dependency availability.
343
+ // before a newly started keeper-terminal snapshots native dependency availability.
174
344
  await preflightOptionalPty(updateServiceContext(dir));
175
345
  const tasks = new TaskRegistry();
176
346
  // Attaches both the server-driven update path (onUpdate below) and axom update's manual path to
@@ -194,35 +364,223 @@ export async function runDaemon(options = {}) {
194
364
  // invoked*, not on what the host merely supports.
195
365
  serviceLifecycle: resolveServiceLifecycle(options),
196
366
  });
367
+ localState.update = updates;
197
368
  // One ACP client for every external agent; the roster it reports is the ACP agent registry, keyed
198
369
  // by registry agent id (ruling R2) — the same key `agent_open.bridge` now carries.
199
- const acp = new AcpAgents({ cacheDir: dir });
200
- const agents = new AgentSessionManager(acp, null, path.join(dir, 'agents'), undefined, tasks);
370
+ // Live sessions run in keeper-acp (spec §5.3) — the adapters outlive the daemon there — or, when it cannot be reached, in the
371
+ // daemon itself through the local fallback (§5.7). Short-lived connections (list, delete, verify, login) stay direct children.
372
+ const acpLauncher = new KeeperLauncher({ configDir: dir, workstationId: cfg.workstationId, server: cfg.server });
373
+ const acpTransport = new KeeperTransport(new AcpKeeperClient({ launcher: acpLauncher }));
374
+ // Sessions keeper-acp still runs from a previous daemon (a restart, an upgrade, a crash) are adopted where they are — across
375
+ // keeper-acp generations (spec §5.5, §6.3) — before the agent channel's reconnect offer is answered.
376
+ const acpGenerations = keeperGenerations({ launcher: acpLauncher, current: acpTransport });
377
+ const acp = new AcpAgents({ cacheDir: dir, serverOrigin: cfg.server, transports: { keeper: acpTransport }, keeperGenerations: acpGenerations });
378
+ const agents = new AgentSessionManager(acp, null, path.join(dir, 'agents'), tasks);
379
+ void agents.adoptSessions();
201
380
  let bridges = await acp.roster();
381
+ // A scan's roster (and a pushed one) also carries the adapter builds live sessions run and how many could move now (spec §6.4,
382
+ // Task 25): keeper-acp's process labels joined with this daemon's sessions. The hello's roster above does not — it is not a scan.
383
+ const scanRoster = () => acp.roster({ sessions: (session) => agents.rosterSessionState(session) });
202
384
  let supervisor;
203
385
  // Plaintext WebSockets are only for an explicitly opted-in local development fixture. Production
204
386
  // ticket handshakes must be WSS because the ticket is the data-channel key derivation material.
205
387
  const allowInsecureWs = process.env.AXOM_WORKSTATION_ALLOW_INSECURE_WS === '1';
206
- const computer = new ComputerChannel({ server: cfg.server, allowInsecureWs, taskRegistry: tasks,
207
- toolsConfig: { configDir: dir, workstationId: cfg.workstationId, server: cfg.server, roots: (cfg.workspaces ?? []).map(workspace => workspace.cwd), browserBinding: cfg.browserBinding },
388
+ // Files: one daemon-owned host process (files/host.ts) serves file_*_request frames and files.* tool requests.
389
+ const home = os.homedir();
390
+ const playgroundCwd = path.join(home, '.axom', 'playground');
391
+ await fs.mkdir(playgroundCwd, { recursive: true });
392
+ let roots = workspaceRoots(cfg, home);
393
+ let hostRoots = [];
394
+ const filesHost = new FilesHostClient({
395
+ configDir: dir,
396
+ options: async () => { hostRoots = await existingDirectories(roots); return { home, roots: hostRoots }; },
397
+ onActivityChanged: () => tasks.externalActivityChanged(),
398
+ log: (line) => console.error(`${new Date().toISOString()} ${line}`),
399
+ });
400
+ tasks.externalActivity = () => filesHost.activity();
401
+ filesHost.start();
402
+ const stopRootsPoll = startExclusivePoll(async () => {
403
+ try {
404
+ roots = workspaceRoots(await loadClientConfig(dir), home);
405
+ const current = await existingDirectories(roots);
406
+ if (JSON.stringify(current) !== JSON.stringify(hostRoots))
407
+ filesHost.reconfigure();
408
+ }
409
+ catch { /* an unreadable config keeps the current host */ }
410
+ }, FILES_ROOTS_POLL_MS);
411
+ // tools.info is aggregated here and starts no keeper: its terminal fields come from keeper-terminal's hello when that keeper
412
+ // already runs, else from the cached node-pty check. terminal.* go through the TerminalRouter (keeper-terminal, or a legacy
413
+ // tools keeper an older CLI left running with live PTYs: served until they end, then stopped). browser.* keep the
414
+ // browser-kind ToolsClient path. Nothing here constructs a terminal-kind ToolsClient: its request() would start the legacy keeper.
415
+ const toolsConfig = { configDir: dir, workstationId: cfg.workstationId, server: cfg.server, roots, browserBinding: cfg.browserBinding };
416
+ const browserTools = new ToolsClient(toolsConfig, 'browser');
417
+ const ptyStatus = cachedPtyStatus();
418
+ // keeper-terminal generations (Task 26): new work goes to the serving generation; PTYs and execs an older generation still runs
419
+ // after an upgrade are routed to it by owner until the replacement policy retires it.
420
+ const terminalLauncher = new KeeperLauncher({ configDir: dir, workstationId: cfg.workstationId, server: cfg.server });
421
+ const terminalKeepers = new TerminalGenerations({ launcher: terminalLauncher });
422
+ const legacyKeeper = new LegacyToolsKeeper({
423
+ tools: toolsConfig,
424
+ recoverUploads: (uploads) => filesHost.recoverUploads(uploads),
425
+ log: (line) => console.error(`${new Date().toISOString()} ${line}`),
426
+ });
427
+ void legacyKeeper.detect();
428
+ const stopLegacyPoll = startExclusivePoll(() => legacyKeeper.tick(), LEGACY_TICK_MS);
429
+ const terminals = new TerminalRouter({
430
+ kits: terminalKeepers,
431
+ legacy: legacyKeeper,
432
+ launch: async (cwd) => ({ cwd: await filesHost.terminalCwd(cwd), shell: terminalShell(), env: terminalEnv() }),
433
+ });
434
+ const workstationTools = new WorkstationTools({
435
+ files: filesHost,
436
+ terminal: (method, params) => terminals.request(method, params),
437
+ browser: (method, params) => browserTools.request(method, params),
438
+ info: () => aggregateToolsInfo({
439
+ files: async () => filesHost.info() ?? computeFilesInfo(home, roots),
440
+ terminal: () => terminalKeepers.terminalStatus(ptyStatus),
441
+ browser: (method, params) => browserTools.request(method, params),
442
+ }),
443
+ });
444
+ // exec_request: keeper-terminal (durable) when available, else in this process; argv/env/deadline/shaping decided here.
445
+ // Execs a previous daemon left in keeper-terminal are recovered first: wire ≤ 4 ones are stopped (their channel is gone), wire v5
446
+ // ones are followed for the server to re-attach (exec_attach on the next computer channel).
447
+ // Keeper replacement (spec §8.3, Task 26): what the heartbeat reports of each keeper generation, and what happens to an older one.
448
+ const drainKeeper = oneShotDrain((kind, gen) => (kind === 'acp' ? acpLauncher : terminalLauncher).pathsFor(kind, gen));
449
+ const keeperPolicy = new KeeperPolicy({
450
+ dir: path.join(dir, 'keepers'),
451
+ probe: oneShotProbe((kind, gen) => (kind === 'acp' ? acpLauncher : terminalLauncher).pathsFor(kind, gen)),
452
+ terminal: {
453
+ currentGen: () => terminalKeepers.currentGen(),
454
+ live: async () => (await terminalKeepers.running()).map((generation) => generation.gen),
455
+ manifest: () => terminalKeepers.manifest(),
456
+ stop: async (gen, mode) => {
457
+ const result = await terminalLauncher.stop('terminal', gen, mode);
458
+ if (result.stopped)
459
+ terminalKeepers.forget(gen);
460
+ return result;
461
+ },
462
+ reachable: async (gen) => (await terminalKeepers.client(gen)).runningHello(),
463
+ drain: (gen) => drainKeeper('terminal', gen),
464
+ },
465
+ legacy: legacyKeeper,
466
+ acp: {
467
+ currentGen: () => acpGenerations.current(),
468
+ live: () => acpGenerations.live(),
469
+ manifest: () => readKeeperManifest(path.join(defaultDistDir(), 'acp')),
470
+ stop: (gen, mode) => acpLauncher.stop('acp', gen, mode),
471
+ moveOff: (gens) => agents.moveOffOlderKeeperGenerations(undefined, gens),
472
+ cancelMoves: async (refs) => { for (const sessionRef of refs)
473
+ await agents.cancelSwitch({ sessionRef }); },
474
+ retireIdle: () => acp.retireIdleGenerations(),
475
+ session: (ref) => agents.sessionView(ref),
476
+ },
477
+ agentName: (agentId) => bridges.find((bridge) => bridge.type === agentId)?.displayName || agentId,
478
+ legacyShell: () => terminalShell(),
479
+ log: (line) => console.error(`${new Date().toISOString()} ${line}`),
480
+ });
481
+ terminalKeepers.picker = (current, older) => keeperPolicy.terminalServing(current, older);
482
+ const stopKeeperPolicy = startExclusivePoll(() => keeperPolicy.tick(), KEEPER_POLICY_TICK_MS);
483
+ // Spec §3.1: keeper sockets that fell back to /tmp (a long configDir path) are touched so a /tmp cleaner never reaps them.
484
+ const stopSocketTouch = startExclusivePoll(async () => {
485
+ await acpLauncher.touchSockets('acp');
486
+ await terminalLauncher.touchSockets('terminal');
487
+ }, KEEPER_POLICY_TICK_MS);
488
+ void keeperPolicy.tick();
489
+ const execPolicy = new ExecPolicy({ generations: terminalKeepers, log: (line) => console.error(`${new Date().toISOString()} ${line}`) });
490
+ void execPolicy.recover();
491
+ const computer = new ComputerChannel({ server: cfg.server, allowInsecureWs, taskRegistry: tasks, browserBinding: cfg.browserBinding,
492
+ runExec: (request, onOutput, signal, hooks) => execPolicy.run(request, onOutput, signal, hooks),
493
+ execs: execPolicy, log: (line) => console.error(`${new Date().toISOString()} ${line}`),
494
+ tools: (method, params) => workstationTools.request(method, params),
495
+ runFrame: (request, signal) => workstationTools.runFrame(request, signal),
208
496
  });
209
497
  const metrics = new SystemMetricsSampler(cfg.workspaces ?? []);
210
498
  await metrics.start();
499
+ const toolchain = new ToolchainProbe(() => terminalShell(), () => terminalEnv());
500
+ toolchain.start();
501
+ const desktopSupervisor = desktopProvider && !options.onDesktopCommand ? new DesktopSessionSupervisor({ provider: desktopProvider,
502
+ sockets: new DeviceSockets(cfg.server, process.env.AXOM_DESKTOP_ALLOW_PRIVATE_WS === '1'),
503
+ report: (generation, frame) => management.sendDesktopState(generation, frame),
504
+ refreshCapabilities: generation => management.refreshDesktopCapabilities(generation), }) : undefined;
505
+ let rdGeneration;
506
+ const rdUpkeep = process.platform === 'linux' && desktopSupervisor ? new RdUpkeep({
507
+ synchronize: async () => {
508
+ if (!rdGeneration || shutdown.signal.aborted)
509
+ return;
510
+ const { synchronizeRd } = await import('./rd/update.js');
511
+ return synchronizeRd(dir, cfg.server, { deferActivation: true });
512
+ },
513
+ whenIdle: operation => desktopSupervisor.whenIdle(async () => {
514
+ if (!tasks.canUpdate() || !rdGeneration || shutdown.signal.aborted)
515
+ return;
516
+ await operation();
517
+ }),
518
+ refresh: () => { if (rdGeneration)
519
+ management.refreshDesktopCapabilities(rdGeneration); },
520
+ log: message => console.error(`${new Date().toISOString()} ${message}`),
521
+ }) : undefined;
522
+ const stopRdUpkeep = rdUpkeep ? startExclusivePoll(() => rdUpkeep.tick(), 60_000) : () => { };
211
523
  const management = new ManagementChannel({
212
524
  server: cfg.server, workstationId: cfg.workstationId, workstation: cfg.workstationId, clientKey, serverPub,
213
525
  bridges: () => bridges,
214
526
  workspaces: cfg.workspaces,
527
+ playgroundCwd,
215
528
  metrics: () => metrics.snapshot(),
529
+ toolchain: () => toolchain.current(),
216
530
  allowInsecureWs,
217
- onReady: (ready) => updates.managementReady(ready.release, ready.generation),
531
+ onReady: async (ready) => {
532
+ await updates.managementReady(ready.release, ready.generation);
533
+ await activateRdAfterReadiness?.();
534
+ if (ready.selectedWire >= 9)
535
+ desktopSupervisor?.managementReady(ready.generation);
536
+ rdGeneration = ready.selectedWire >= 9 ? ready.generation : undefined;
537
+ void rdUpkeep?.tick();
538
+ },
539
+ desktopProvider,
540
+ onDesktopCommand: options.onDesktopCommand ?? ((frame, generation) => desktopSupervisor?.dispatch(frame, generation) ?? Promise.resolve()),
541
+ onClosed: generation => {
542
+ if (rdGeneration === generation)
543
+ rdGeneration = undefined;
544
+ desktopSupervisor?.managementClosed(generation);
545
+ options.onDesktopManagementClosed?.(generation);
546
+ },
218
547
  onUpdate: (command) => updates.handle(command),
219
548
  onExternalAgentsScan: async () => {
220
- bridges = await acp.roster();
549
+ bridges = await scanRoster();
221
550
  return bridges;
222
551
  },
223
552
  snapshot: () => buildManagementSnapshot(supervisor, tasks, updates),
553
+ keepers: () => buildKeeperStatuses(keeperPolicy, () => tasks.countedKeeperExecs),
554
+ onKeeperControl: (frame) => keeperPolicy.control(frame),
224
555
  });
556
+ options.onDesktopManagement?.(management);
225
557
  const agent = new AgentChannel({ server: cfg.server, agents, bridges: () => bridges, allowInsecureWs });
558
+ // Spec §6.2: first take over what an older CLI left (`_npx` copies, pre-build binary dirs) so agents
559
+ // that worked keep working; then §6.1: a version this CLI certifies (runtime.json/catalog pin) or a
560
+ // changed patch set moves an installed agent's current build forward by itself ("Updated by Axom").
561
+ // In the background: either may copy or download, an open of an agent not done yet waits on that
562
+ // agent's lock, and running sessions keep the build they were spawned from either way. A changed
563
+ // roster reaches the server the way a scan's does; offline, the next hello carries it.
564
+ void maintainAdapterBuilds(acp, async () => {
565
+ bridges = await scanRoster();
566
+ management.pushRoster(bridges);
567
+ });
568
+ // A quota capability learned mid-flight (this process's first spawn of an agent) changes what the
569
+ // roster would say; push it the way a build change does. Coalesced: several agents flipping
570
+ // around one burst of opens share a single scan, and a flip during that scan queues one more.
571
+ let rosterPushQueued = false;
572
+ acp.onRosterChanged(() => {
573
+ if (rosterPushQueued)
574
+ return;
575
+ rosterPushQueued = true;
576
+ void Promise.resolve().then(async () => {
577
+ rosterPushQueued = false;
578
+ bridges = await scanRoster();
579
+ management.pushRoster(bridges);
580
+ }).catch((error) => {
581
+ console.warn(`[acp-builds] roster push after capability change failed: ${String(error)}`);
582
+ });
583
+ });
226
584
  supervisor = new ConnectionSupervisor({
227
585
  management, computer, agent,
228
586
  // Timestamped here rather than inside the supervisor: the daemon owns this log's shape. The
@@ -231,7 +589,15 @@ export async function runDaemon(options = {}) {
231
589
  log: (message) => console.log(`${new Date().toISOString()} ${message}`),
232
590
  });
233
591
  const stopIdleSweep = startIdleSweep(agents);
234
- const shutdown = new AbortController();
592
+ const stopSwitchUpkeep = startSwitchUpkeep(agents, acp);
593
+ localState.snapshot = async () => {
594
+ const channels = supervisor.channelSnapshot();
595
+ const active = await buildManagementSnapshot(supervisor, tasks, updates);
596
+ const keepers = await buildKeeperStatuses(keeperPolicy, () => tasks.countedKeeperExecs);
597
+ const sum = (key) => keepers?.reduce((n, k) => n + (k.metrics[key] ?? 0), 0) ?? 0;
598
+ return { ...stoppedRuntime(), connection: channels.management === 'CONNECTED' ? 'online' : 'reconnecting',
599
+ activity: keepers === null ? null : { commands: Math.max(0, active.activeShellTasks + sum('execRunning') - sum('execCounted')), terminals: sum('ptyOpen'), agents: active.activeAgentTasks + sum('sessions'), transfers: 0 } };
600
+ };
235
601
  const stop = () => shutdown.abort();
236
602
  process.once('SIGINT', stop);
237
603
  process.once('SIGTERM', stop);
@@ -239,9 +605,24 @@ export async function runDaemon(options = {}) {
239
605
  await supervisor.run(shutdown.signal);
240
606
  }
241
607
  finally {
608
+ stopRdUpkeep();
609
+ rdUpkeep?.stop();
610
+ await desktopSupervisor?.close();
242
611
  process.removeListener('SIGINT', stop);
243
612
  process.removeListener('SIGTERM', stop);
244
613
  stopIdleSweep();
614
+ stopSwitchUpkeep();
245
615
  metrics.stop();
616
+ toolchain.stop();
617
+ stopRootsPoll();
618
+ stopLegacyPoll();
619
+ stopKeeperPolicy();
620
+ stopSocketTouch();
621
+ terminalKeepers.disconnect(); // only the connections: keeper-terminal and its PTYs outlive the daemon
622
+ // keeper-hosted sessions are detached (head checkpoint, adapter untouched, no failed turn); the others checkpoint (spec §5.5.5)
623
+ await agents.detachAll();
624
+ acp.closeTransports(); // keeper-acp's adapters outlive the daemon; local-fallback adapters die with it (spec §5.7)
625
+ // Bounded: a graceful host shutdown, then SIGKILL (a hung IO cannot hold the daemon's exit).
626
+ await filesHost.stop();
246
627
  }
247
628
  }
@@ -1,10 +1,17 @@
1
1
  import os from 'node:os';
2
+ import { BindingStore } from './control/bindingStore.js';
3
+ import { stopRuntime } from './control/runtimeClient.js';
4
+ import { LocalControlError } from './control/protocol.js';
5
+ import { normalizeWorkstationServer } from './control/serverIdentity.js';
2
6
  import path from 'node:path';
3
7
  import { fileURLToPath } from 'node:url';
4
- import { clientDir, loadClientConfig } from './config.js';
8
+ import { clientDir, loadClientConfig, updateServiceContext } from './config.js';
5
9
  import { createServiceManager } from './service/installer.js';
6
10
  import { buildServicePath } from './service/types.js';
7
11
  import { loginShellPath } from './service/loginShell.js';
12
+ import { KeeperLauncher } from './keepers/kit/launcher.js';
13
+ import { stopKeepers } from './keepers/stop.js';
14
+ import { assertRuntimeRestartSafe } from './control/restartGuard.js';
8
15
  const __filename = fileURLToPath(import.meta.url);
9
16
  /**
10
17
  * The package's real npm bin entry (`package.json`'s `bin.axom`), given the directory a compiled
@@ -34,24 +41,41 @@ export function serviceContext() {
34
41
  throw new Error('Axomwork Workstation user service supervision is supported only on macOS and Linux.');
35
42
  }
36
43
  const directory = path.dirname(__filename);
37
- const nodeDirectory = path.dirname(process.execPath);
44
+ const managed = updateServiceContext();
45
+ const nodeDirectory = path.dirname(managed.nodePath);
38
46
  return {
39
47
  platform: process.platform,
40
48
  username: os.userInfo().username,
41
49
  userId: process.getuid?.() ?? os.userInfo().uid,
42
50
  homeDir: os.homedir(),
43
- nodePath: process.execPath,
51
+ nodePath: managed.nodePath,
44
52
  cliPath: CLI_PATH,
45
- npmPath: process.env.AXOM_WORKSTATION_NPM_PATH ?? path.join(nodeDirectory, 'npm'),
53
+ npmPath: managed.npmPath,
46
54
  // dist/workstation/ is nested under lib/node_modules/@platless/axom-cli; climb to npm's prefix.
47
- npmPrefix: process.env.AXOM_WORKSTATION_NPM_PREFIX ?? path.resolve(directory, '../../../../../..'),
55
+ npmPrefix: managed.npmPrefix,
48
56
  configDir: clientDir(),
49
- pathEnv: buildServicePath(process.env.PATH, nodeDirectory),
57
+ pathEnv: buildServicePath(managed.env?.PATH ?? process.env.PATH, nodeDirectory),
58
+ ...(managed.managed ? { npmConfigPrefix: managed.managed.prefix } : {}),
50
59
  };
51
60
  }
61
+ /** The workstation's keepers stopped (see {@link DaemonControllerDeps.stopKeepers}); an unregistered client has none. */
62
+ async function stopRegisteredKeepers() {
63
+ const configDir = clientDir();
64
+ const config = await loadClientConfig(configDir).catch(() => null);
65
+ if (!config?.workstationId || !config.server)
66
+ return;
67
+ await stopKeepers(new KeeperLauncher({ configDir, workstationId: config.workstationId, server: config.server }), {
68
+ log: (line) => console.error(line),
69
+ });
70
+ }
52
71
  const defaultDeps = {
72
+ identity: () => new BindingStore(clientDir()).inspect(),
73
+ runtime: async () => (await (await import('./control/inspect.js')).inspectWorkstation(clientDir())).runtime,
74
+ shutdown: generation => stopRuntime(clientDir(), generation),
75
+ restartSafe: generation => assertRuntimeRestartSafe(clientDir(), generation),
53
76
  service: (context) => createServiceManager(context ?? serviceContext()),
54
77
  config: () => loadClientConfig(clientDir()),
78
+ stopKeepers: stopRegisteredKeepers,
55
79
  };
56
80
  /**
57
81
  * User services own the daemon lifecycle. Deliberately no PID-file fallback remains: a stale
@@ -59,6 +83,22 @@ const defaultDeps = {
59
83
  */
60
84
  export function createDaemonController(overrides = {}) {
61
85
  const deps = { ...defaultDeps, ...overrides };
86
+ async function checked(options) {
87
+ const runtime = await deps.runtime();
88
+ if (runtime.state === 'unknown')
89
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Cannot verify the running workstation');
90
+ if (options) {
91
+ const identity = await deps.identity();
92
+ if (identity.revision !== options.guard.revision || runtime.generation !== options.guard.runtimeGeneration)
93
+ throw new LocalControlError('BINDING_CONFLICT', 'Workstation changed; check it again');
94
+ }
95
+ if (runtime.state === 'running') {
96
+ const config = await deps.config();
97
+ if (!runtime.binding || runtime.binding.server !== normalizeWorkstationServer(config.server) || runtime.binding.workstationId !== config.workstationId)
98
+ throw new LocalControlError('INSTANCE_OCCUPIED', 'Another workstation binding is running');
99
+ }
100
+ return runtime;
101
+ }
62
102
  return {
63
103
  async install() {
64
104
  // The definition is written from the login shell's PATH when one can be had, unioned with
@@ -73,16 +113,65 @@ export function createDaemonController(overrides = {}) {
73
113
  : base;
74
114
  await deps.service(context).install({ interactive: true });
75
115
  },
76
- async uninstall() { await deps.service().uninstall(); },
77
- async start() { await deps.service().start(); },
78
- async stop() { await deps.service().stop(); },
79
- async restart() { await deps.service().restart(); },
116
+ // The workstation is stopped by the user: what its keepers host ends too (the daemon first, so nothing relaunches them).
117
+ // In a `finally`: a service that is not installed (or fails to stop) must not leave the keepers running.
118
+ async uninstall() { try {
119
+ await deps.service().uninstall();
120
+ }
121
+ finally {
122
+ await deps.stopKeepers();
123
+ } },
124
+ async start(options) { if ((await checked(options)).state === 'running')
125
+ return; await deps.service().start(); },
126
+ async stop(options) { const runtime = await checked(options); try {
127
+ if (runtime.mode === 'foreground' && runtime.generation)
128
+ await deps.shutdown(runtime.generation);
129
+ else
130
+ await deps.service().stop();
131
+ }
132
+ finally {
133
+ await deps.stopKeepers();
134
+ } },
135
+ // A restart keeps the keepers: terminals, commands and agent sessions go on under the next daemon.
136
+ async restart(options) {
137
+ const runtime = await checked(options);
138
+ if (runtime.state === 'running' && runtime.generation)
139
+ await deps.restartSafe(runtime.generation);
140
+ if (runtime.mode === 'foreground' && runtime.generation)
141
+ await deps.shutdown(runtime.generation);
142
+ await deps.service().restart();
143
+ },
144
+ async setEnabled(enabled, options) {
145
+ const runtime = await checked(options);
146
+ const service = deps.service();
147
+ if (!service.setEnabled)
148
+ throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Service enablement is unavailable');
149
+ if (enabled)
150
+ await service.setEnabled(true);
151
+ else {
152
+ try {
153
+ await service.setEnabled(false);
154
+ if (runtime.mode === 'foreground' && runtime.generation) {
155
+ const current = await deps.runtime();
156
+ if (current.state === 'running' && current.generation === runtime.generation)
157
+ await deps.shutdown(runtime.generation);
158
+ else if (current.state !== 'stopped')
159
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'The running workstation changed during shutdown');
160
+ }
161
+ }
162
+ finally {
163
+ await deps.stopKeepers();
164
+ }
165
+ }
166
+ },
80
167
  async status() {
168
+ const runtime = await deps.runtime();
169
+ if (runtime.state === 'running' && runtime.binding)
170
+ return { running: true, server: runtime.binding.server, workstationId: runtime.binding.workstationId };
171
+ if (runtime.state === 'unknown')
172
+ return { running: false, uncertain: true };
81
173
  const state = await deps.service().status();
82
- if (!state.running)
83
- return { running: false };
84
- const config = await deps.config().catch(() => null);
85
- return { running: true, server: config?.server, workstationId: config?.workstationId };
174
+ return state.running ? { running: true, uncertain: true } : { running: false };
86
175
  },
87
176
  };
88
177
  }