@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,145 @@
1
+ import { constants, promises as fs } from 'node:fs';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import path from 'node:path';
4
+ import { assertLockOwnership, withPrivateLock } from '../control/locks.js';
5
+ import { privateDirectory } from '../keepers/kit/paths.js';
6
+ import { rdStateSchema } from '../rd/state.js';
7
+ export function rdStateDigest(value) { return createHash('sha256').update(value).digest('hex'); }
8
+ function snapshotPath(directory, commandId) { return path.join(directory, 'update', `rd-rollback-${rdStateDigest(commandId)}.json`); }
9
+ export async function readPrivateRdFile(file, maxBytes = 131072) {
10
+ let handle;
11
+ try {
12
+ handle = await fs.open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
13
+ }
14
+ catch (error) {
15
+ if (error.code === 'ENOENT')
16
+ return null;
17
+ throw error;
18
+ }
19
+ try {
20
+ const info = await handle.stat();
21
+ if (!info.isFile() || info.uid !== process.getuid?.() || info.mode & 0o077 || info.size > maxBytes)
22
+ throw new Error('Unsafe RD rollback state');
23
+ return await handle.readFile('utf8');
24
+ }
25
+ finally {
26
+ await handle.close();
27
+ }
28
+ }
29
+ async function writePrivate(file, contents) {
30
+ await privateDirectory(path.dirname(file));
31
+ const staging = path.join(path.dirname(file), `.rd-rollback-${randomUUID()}`);
32
+ try {
33
+ await fs.writeFile(staging, contents, { mode: 0o600, flag: 'wx' });
34
+ assertLockOwnership();
35
+ await fs.rename(staging, file);
36
+ }
37
+ finally {
38
+ await fs.rm(staging, { force: true });
39
+ }
40
+ }
41
+ export async function snapshotRdForRollback(directory, command) {
42
+ return withPrivateLock(path.join(directory, 'rd', 'install'), async () => {
43
+ const previous = await readPrivateRdFile(path.join(directory, 'rd', 'state.json'), 32768);
44
+ const snapshot = snapshotPath(directory, command.commandId);
45
+ const value = { schemaVersion: 1, commandId: command.commandId, targetVersion: command.release.version,
46
+ releaseSha512: command.release.sha512, previous };
47
+ await writePrivate(snapshot, JSON.stringify(value));
48
+ return { directory, snapshot };
49
+ });
50
+ }
51
+ async function pendingUpdate(directory) {
52
+ const contents = await readPrivateRdFile(path.join(directory, 'update-state.json'));
53
+ return contents === null ? null : JSON.parse(contents);
54
+ }
55
+ export async function guardRdActivation(directory, cliVersion, next) {
56
+ const update = await pendingUpdate(directory);
57
+ if (!update || update.version !== cliVersion || !update.commandId)
58
+ return;
59
+ if (update.phase === 'ROLLED_BACK')
60
+ throw new Error('The CLI update was already rolled back before RD activation');
61
+ if (!update.releaseSha512)
62
+ return;
63
+ const planFile = path.join(directory, 'update', `watchdog-${rdStateDigest(update.commandId)}.json`);
64
+ const planContents = await readPrivateRdFile(planFile);
65
+ if (planContents === null)
66
+ return;
67
+ const plan = JSON.parse(planContents);
68
+ if (plan.commandId !== update.commandId || plan.targetVersion !== update.version)
69
+ throw new Error('RD watchdog plan does not match the pending CLI update');
70
+ const current = await readPrivateRdFile(path.join(directory, 'rd', 'state.json'), 32768);
71
+ if (current === null)
72
+ throw new Error('RD state disappeared before activation');
73
+ const guard = { commandId: update.commandId, targetVersion: update.version, releaseSha512: update.releaseSha512, previousSha256: rdStateDigest(current) };
74
+ if (!plan.rdRollback)
75
+ return await authenticatedRdActivation(directory, guard) ? undefined : guard;
76
+ const expected = snapshotPath(directory, update.commandId);
77
+ if (plan.rdRollback.directory !== directory || plan.rdRollback.snapshot !== expected)
78
+ throw new Error('RD rollback snapshot path does not match this update');
79
+ const contents = await readPrivateRdFile(expected);
80
+ if (contents === null)
81
+ throw new Error('RD rollback snapshot is missing');
82
+ const saved = JSON.parse(contents);
83
+ if (saved.schemaVersion !== 1 || saved.commandId !== update.commandId || saved.targetVersion !== update.version || saved.releaseSha512 !== update.releaseSha512)
84
+ throw new Error('RD rollback snapshot belongs to another CLI update');
85
+ const nextDigest = rdStateDigest(JSON.stringify(rdStateSchema.parse(next)));
86
+ if (saved.previous !== current && saved.activatedSha256 !== rdStateDigest(current))
87
+ return guard;
88
+ await writePrivate(expected, JSON.stringify({ ...saved, activatedSha256: nextDigest, activatedIdentity: undefined }));
89
+ }
90
+ async function stateIdentity(directory) {
91
+ const info = await fs.lstat(path.join(directory, 'rd', 'state.json'), { bigint: true });
92
+ return `${info.dev}:${info.ino}:${info.birthtimeNs}:${info.mtimeNs}`;
93
+ }
94
+ export async function completeRdActivation(directory, cliVersion) {
95
+ const update = await pendingUpdate(directory);
96
+ if (!update?.commandId || update.version !== cliVersion)
97
+ return;
98
+ const file = snapshotPath(directory, update.commandId);
99
+ const contents = await readPrivateRdFile(file);
100
+ if (contents === null)
101
+ return;
102
+ const saved = JSON.parse(contents);
103
+ if (saved.commandId !== update.commandId || saved.targetVersion !== update.version || saved.releaseSha512 !== update.releaseSha512 || !saved.activatedSha256)
104
+ return;
105
+ const active = await readPrivateRdFile(path.join(directory, 'rd', 'state.json'), 32768);
106
+ if (active === null || rdStateDigest(active) !== saved.activatedSha256)
107
+ return;
108
+ await writePrivate(file, JSON.stringify({ ...saved, activatedIdentity: await stateIdentity(directory) }));
109
+ }
110
+ export async function authenticatedRdActivation(directory, guard) {
111
+ const update = await pendingUpdate(directory);
112
+ if (update?.phase !== 'SUCCEEDED' || update.commandId !== guard.commandId || update.version !== guard.targetVersion || update.releaseSha512 !== guard.releaseSha512)
113
+ return false;
114
+ const contents = await readPrivateRdFile(path.join(directory, 'update', 'restart-ready.json'));
115
+ if (contents === null)
116
+ return false;
117
+ const marker = JSON.parse(contents);
118
+ return marker.commandId === guard.commandId && marker.version === guard.targetVersion && typeof marker.generation === 'string' && marker.generation.length > 0;
119
+ }
120
+ export async function restoreRdAfterRollback(reference, commandId, targetVersion) {
121
+ const directory = reference.directory;
122
+ if (reference.snapshot !== snapshotPath(directory, commandId))
123
+ throw new Error('RD rollback snapshot path does not match this update');
124
+ await withPrivateLock(path.join(directory, 'rd', 'install'), async () => {
125
+ const update = await pendingUpdate(directory);
126
+ if (update?.commandId !== commandId || update.version !== targetVersion)
127
+ return;
128
+ const contents = await readPrivateRdFile(reference.snapshot);
129
+ if (contents === null)
130
+ throw new Error('RD rollback snapshot is missing');
131
+ const saved = JSON.parse(contents);
132
+ if (saved.schemaVersion !== 1 || saved.commandId !== commandId || saved.targetVersion !== targetVersion || saved.releaseSha512 !== update.releaseSha512)
133
+ throw new Error('RD rollback snapshot belongs to another CLI update');
134
+ if (!saved.activatedSha256)
135
+ return;
136
+ const active = await readPrivateRdFile(path.join(directory, 'rd', 'state.json'), 32768);
137
+ if (active === null || rdStateDigest(active) !== saved.activatedSha256)
138
+ return;
139
+ if (saved.activatedIdentity && await stateIdentity(directory) !== saved.activatedIdentity)
140
+ return;
141
+ if (saved.previous === null)
142
+ throw new Error('RD activation has no previous state to restore');
143
+ await writePrivate(path.join(directory, 'rd', 'state.json'), saved.previous);
144
+ });
145
+ }
@@ -124,7 +124,7 @@ export function lifecyclePolicy(lifecycle) {
124
124
  export function createUpdateCoordinator(deps) {
125
125
  const policy = lifecyclePolicy(deps.serviceLifecycle);
126
126
  const updateContext = updateServiceContext(deps.dir);
127
- const npm = new NpmInstaller(updateContext);
127
+ const npm = new NpmInstaller({ ...updateContext, installationLockHeld: deps.installationLockHeld });
128
128
  // This cache also resumes the restart handshake after service supervision starts a new daemon.
129
129
  const releaseCache = new LastKnownGoodCache({
130
130
  ...updateContext, runningVersion: CLIENT_VERSION, pack: (root, destination) => npm.pack(root, destination),
@@ -180,7 +180,8 @@ export function createUpdateCoordinator(deps) {
180
180
  const log = openSync(`${deps.dir}/daemon.log`, 'a');
181
181
  let child;
182
182
  try {
183
- child = spawn(process.execPath, [deps.cliPath, ...PROVISION_SERVICE_ARGV], {
183
+ child = spawn(updateContext.nodePath, [deps.cliPath, ...PROVISION_SERVICE_ARGV], {
184
+ ...(updateContext.env ? { env: updateContext.env } : {}),
184
185
  detached: true,
185
186
  stdio: ['ignore', log, log],
186
187
  });
@@ -10,6 +10,14 @@ export class TaskRegistry {
10
10
  idleWaiters = new Set();
11
11
  stopping = null;
12
12
  accepting = true;
13
+ /** Keeper-terminal execs also counted in `activeShellTasks` right now (see {@link countKeeperExec}). */
14
+ keeperExecsCounted = 0;
15
+ /**
16
+ * Work owned by another process that a daemon restart would interrupt (the files host's in-progress uploads plus
17
+ * running media encodes), sampled **now** on every snapshot — never an edge counter. Its owner calls
18
+ * [externalActivityChanged] when the level may have dropped, so a drain waiting on it wakes.
19
+ */
20
+ externalActivity = () => 0;
13
21
  /**
14
22
  * 忙碌是**现在采出来的**,不是攒出来的。
15
23
  *
@@ -24,6 +32,11 @@ export class TaskRegistry {
24
32
  snapshot() {
25
33
  const activeAgentRefs = new Set();
26
34
  for (const [sessionRef, session] of this.agentSessions) {
35
+ // Spec §8.2: `activeAgentTasks` counts only agent work a daemon restart would cut. A keeper-hosted (durable) session is not
36
+ // such work — its turn goes on in keeper-acp and the next daemon adopts it — so a busy one neither holds a graceful drain nor
37
+ // blocks an automatic patch. Fallback sessions (in this process) and in-flight open reservations are counted.
38
+ if (session.durable)
39
+ continue;
27
40
  // 一个抛异常的采样器不得让心跳和更新准入一起垮掉;读不出来就当它不主张忙碌,
28
41
  // 与未知 vendor 值的处置同一条推理。
29
42
  let busy = false;
@@ -38,7 +51,16 @@ export class TaskRegistry {
38
51
  }
39
52
  for (const sessionRef of this.agentOpenReservations.keys())
40
53
  activeAgentRefs.add(sessionRef);
41
- return { activeShellTasks: this.shellTasks.size, activeAgentTasks: activeAgentRefs.size };
54
+ let external = 0;
55
+ // Same rule as the session sampler above: a failing sampler claims nothing rather than taking the heartbeat down.
56
+ try {
57
+ const sampled = this.externalActivity();
58
+ external = Number.isSafeInteger(sampled) && sampled > 0 ? sampled : 0;
59
+ }
60
+ catch {
61
+ external = 0;
62
+ }
63
+ return { activeShellTasks: this.shellTasks.size + external, activeAgentTasks: activeAgentRefs.size };
42
64
  }
43
65
  canUpdate() {
44
66
  const snapshot = this.snapshot();
@@ -79,7 +101,11 @@ export class TaskRegistry {
79
101
  for (const waiter of [...this.idleWaiters])
80
102
  waiter();
81
103
  }
82
- /** Run one computer task under an abort signal which force-update owns. The method name preserves old callers. */
104
+ /**
105
+ * Run one computer task under an abort signal which force-update owns. The method name preserves old callers.
106
+ * `release()` stops counting the task before it ends (work a daemon restart would no longer interrupt, spec §8.2): from then on it
107
+ * is not in `activeShellTasks`, a drain does not wait for it and `stopAll()` does not abort it. Idempotent.
108
+ */
83
109
  async withShellTask(run) {
84
110
  if (!this.accepting)
85
111
  throw new Error('Workstation is not accepting new work');
@@ -87,18 +113,61 @@ export class TaskRegistry {
87
113
  let complete;
88
114
  const cleanedUp = new Promise((resolve) => { complete = resolve; });
89
115
  this.shellTasks.set(controller, cleanedUp);
116
+ let counted = true;
117
+ const release = () => {
118
+ if (!counted)
119
+ return;
120
+ counted = false;
121
+ this.shellTasks.delete(controller);
122
+ complete();
123
+ this.notifyIfIdle();
124
+ };
90
125
  try {
91
- return await run(controller.signal);
126
+ return await run(controller.signal, release);
92
127
  }
93
128
  finally {
129
+ release();
130
+ }
131
+ }
132
+ /**
133
+ * Counts a short window of work in `activeShellTasks` WITHOUT the admission gate and without being stoppable: a drain (graceful
134
+ * or forced) waits for it, nothing aborts it. For work admitted while draining that a restart must not cut in half (a wire v5
135
+ * exec between `exec.start` and its first checkpoint, spec §8.2). Returns the release (idempotent).
136
+ */
137
+ holdShellTask() {
138
+ const controller = new AbortController();
139
+ let complete;
140
+ this.shellTasks.set(controller, new Promise((resolve) => { complete = resolve; }));
141
+ let held = true;
142
+ return () => {
143
+ if (!held)
144
+ return;
145
+ held = false;
94
146
  this.shellTasks.delete(controller);
95
147
  complete();
96
148
  this.notifyIfIdle();
97
- }
149
+ };
98
150
  }
99
- /** Register a live native agent for force-stop, plus how to ask whether it is busy right now. */
100
- registerAgentSession(sessionRef, close, busy) {
101
- this.agentSessions.set(sessionRef, { close, busy });
151
+ /**
152
+ * Marks one exec that runs in keeper-terminal AND is counted in `activeShellTasks` right now — a wire ≤ 4 exec (a shell task for
153
+ * its whole run) or a wire v5 exec before its first checkpoint (held). keeper-terminal's `execRunning` counts it too: the heartbeat
154
+ * reports this number (`execCounted`) so the Workstations page does not count it twice (final review minor 7). Returns the
155
+ * release (idempotent).
156
+ */
157
+ countKeeperExec() {
158
+ this.keeperExecsCounted += 1;
159
+ let counted = true;
160
+ return () => { if (!counted)
161
+ return; counted = false; this.keeperExecsCounted -= 1; };
162
+ }
163
+ /** How many keeper-terminal execs `activeShellTasks` includes now (see {@link countKeeperExec}). */
164
+ get countedKeeperExecs() { return this.keeperExecsCounted; }
165
+ /**
166
+ * Register a live native agent for force-stop, plus how to ask whether it is busy right now. A durable session's `close` must be
167
+ * its detach (see {@link AgentSessionRegistration}).
168
+ */
169
+ registerAgentSession(sessionRef, close, busy, registration = {}) {
170
+ this.agentSessions.set(sessionRef, { close, busy, durable: registration.durable === true });
102
171
  }
103
172
  /**
104
173
  * 采样式判据没有"变空闲"这个事件可以挂钩,所以持有者在可能变空闲时喊一声。
@@ -107,6 +176,10 @@ export class TaskRegistry {
107
176
  sessionBusyChanged() {
108
177
  this.notifyIfIdle();
109
178
  }
179
+ /** The external activity level may have changed (see [externalActivity]). */
180
+ externalActivityChanged() {
181
+ this.notifyIfIdle();
182
+ }
110
183
  /** Atomically admits and counts an asynchronous open before any journal or subprocess await. */
111
184
  tryReserveAgentOpen(sessionRef) {
112
185
  if (!this.accepting)
@@ -126,6 +199,10 @@ export class TaskRegistry {
126
199
  isForceStopping() {
127
200
  return this.stopping !== null;
128
201
  }
202
+ /** Whether `sessionRef` is registered as a keeper-hosted (durable) session: a force stop leaves it admitted (see {@link stopAll}). */
203
+ isDurableSession(sessionRef) {
204
+ return this.agentSessions.get(sessionRef)?.durable === true;
205
+ }
129
206
  unregisterAgentSession(sessionRef) {
130
207
  this.agentSessions.delete(sessionRef);
131
208
  this.notifyIfIdle();
@@ -134,12 +211,29 @@ export class TaskRegistry {
134
211
  * 排干期间拒收新回合。**只管准入**——它不再登记任何东西,忙碌由 [snapshot] 现场采样。
135
212
  * 这两件事以前挤在一个方法里,删计数时极易把准入一起删掉,那会让 drain 期间照收新活。
136
213
  */
137
- tryAdmitTurn(_sessionRef) {
138
- return this.accepting;
214
+ tryAdmitTurn(sessionRef) {
215
+ // Spec §8.2: a keeper-hosted session's prompt is admitted while draining — it enters keeper-acp's log first and a restart
216
+ // continues it; a fallback session's would be cut by the restart.
217
+ return this.accepting || this.agentSessions.get(sessionRef)?.durable === true;
218
+ }
219
+ /**
220
+ * The restart path of an update (Task 21, spec §5.5.5): every durable session is detached (its registered close), so the next daemon
221
+ * adopts it with a head checkpoint; fallback sessions are left as they are (they end with this process, their outboxes kept).
222
+ * Bounded by `timeoutMs`: a restart never waits on a session longer than that. All are attempted even when one fails.
223
+ */
224
+ async detachDurableSessions(timeoutMs = 5_000) {
225
+ const durable = [...this.agentSessions.values()].filter((session) => session.durable);
226
+ const all = Promise.allSettled(durable.map(async (session) => session.close()));
227
+ let timer;
228
+ await Promise.race([all, new Promise((resolve) => { timer = setTimeout(resolve, timeoutMs); })]);
229
+ clearTimeout(timer);
139
230
  }
140
231
  /**
141
232
  * Atomically refuse future work, signal every shell process group through its owning executor,
142
- * and close every native agent. All closes are attempted even when one bridge fails.
233
+ * and close every native agent that runs in this process. A durable (keeper-hosted) session is left alone: it stays registered,
234
+ * attached and admitted (spec §8.2, the same as a graceful drain) — the restart path detaches it right before the hand-off
235
+ * ({@link detachDurableSessions}), and a forced update that fails before that leaves it exactly as it was. All closes are
236
+ * attempted even when one bridge fails.
143
237
  */
144
238
  async stopAll() {
145
239
  if (this.stopping)
@@ -154,7 +248,7 @@ export class TaskRegistry {
154
248
  this.agentOpenReservations.clear();
155
249
  this.notifyIfIdle();
156
250
  const [outcomes] = await Promise.all([
157
- Promise.allSettled([...this.agentSessions.values()].map(async (session) => session.close())),
251
+ Promise.allSettled([...this.agentSessions.values()].filter((session) => !session.durable).map(async (session) => session.close())),
158
252
  Promise.all(shells.map(([, cleanedUp]) => cleanedUp)),
159
253
  ]);
160
254
  const failures = outcomes.filter((outcome) => outcome.status === 'rejected');