@platless/axom-cli 0.5.9 → 0.6.52

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 (258) hide show
  1. package/README.md +25 -6
  2. package/dist/axom-patch/cli/README.md +129 -12
  3. package/dist/axom-patch/cli/account-limits/generate.py +15 -12
  4. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
  5. package/dist/axom-patch/cli/claude-account-limits.json +9 -9
  6. package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
  7. package/dist/axom-patch/cli/codex-account-limits.json +11 -11
  8. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  9. package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
  10. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  11. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  12. package/dist/axom-patch/cli/nightly-task.md +55 -13
  13. package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
  14. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  15. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  16. package/dist/axom-patch/cli/probes/zcode-env-account.mjs +81 -0
  17. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
  18. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1687 -0
  19. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +277 -0
  20. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
  21. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
  22. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  23. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  24. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  25. package/dist/axom-patch/cli/runtime.json +19 -19
  26. package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
  27. package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
  28. package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
  29. package/dist/axom-patch/cli/zcode-env-account.json +58 -0
  30. package/dist/axom-patch/cli/zcode-models.json +9 -10
  31. package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -11
  32. package/dist/cli.js +11 -45
  33. package/dist/commands/auth.js +4 -1
  34. package/dist/commands/knowledge.js +47 -0
  35. package/dist/commands/update.js +126 -3
  36. package/dist/commands/workstation.js +101 -164
  37. package/dist/commands/workstationRegistrations.js +116 -0
  38. package/dist/credentialBroker.js +13 -2
  39. package/dist/credentials.js +16 -0
  40. package/dist/install/claude/commands.js +15 -15
  41. package/dist/keepers/terminal/keeper.mjs +1089 -673
  42. package/dist/keepers/terminal/manifest.json +4 -4
  43. package/dist/mcp.js +1 -1
  44. package/dist/native/darwin-x64/axom-quic +0 -0
  45. package/dist/workstation/acp/acpAgents.js +113 -102
  46. package/dist/workstation/acp/acpClient.js +26 -0
  47. package/dist/workstation/acp/builds/install/npm.js +99 -10
  48. package/dist/workstation/acp/initializeResponse.js +6 -1
  49. package/dist/workstation/acp/managedPatches.js +189 -25
  50. package/dist/workstation/acp/managedRuntime.js +314 -0
  51. package/dist/workstation/acp/outbox.js +12 -5
  52. package/dist/workstation/acp/patchAssets.js +40 -0
  53. package/dist/workstation/acp/permission.js +2 -2
  54. package/dist/workstation/acp/processor/checkpoint.js +26 -34
  55. package/dist/workstation/acp/processor/control.js +7 -80
  56. package/dist/workstation/acp/processor/jsonrpc.js +8 -0
  57. package/dist/workstation/acp/processor/runtime.js +5 -81
  58. package/dist/workstation/acp/processor/session.js +54 -82
  59. package/dist/workstation/acp/processor/state.js +7 -13
  60. package/dist/workstation/acp/processor/step.js +3 -20
  61. package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
  62. package/dist/workstation/acp/processor/transport/local.js +19 -3
  63. package/dist/workstation/acp/processor/updates.js +156 -105
  64. package/dist/workstation/acp/processorSession.js +20 -173
  65. package/dist/workstation/acp/registry/catalog.js +65 -39
  66. package/dist/workstation/acp/registry/launch.js +16 -1
  67. package/dist/workstation/acp/switching.js +0 -3
  68. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  69. package/dist/workstation/acp/test/keeperWorld.js +32 -3
  70. package/dist/workstation/acp/test/raw/sources.js +16 -130
  71. package/dist/workstation/acp/test/realAdapter.js +1 -47
  72. package/dist/workstation/acp/test/scriptAgent.js +11 -3
  73. package/dist/workstation/agentFlow.js +1 -1
  74. package/dist/workstation/agentSessions.js +114 -166
  75. package/dist/workstation/channels/agent.js +9 -1
  76. package/dist/workstation/channels/computer.js +27 -4
  77. package/dist/workstation/channels/management.js +130 -15
  78. package/dist/workstation/channels/url.js +1 -1
  79. package/dist/workstation/config.js +33 -9
  80. package/dist/workstation/connection/encryptedChannel.js +4 -1
  81. package/dist/workstation/control/bindingStore.js +204 -0
  82. package/dist/workstation/control/commands.js +125 -0
  83. package/dist/workstation/control/daemonIdentity.js +143 -0
  84. package/dist/workstation/control/deviceCommands.js +191 -0
  85. package/dist/workstation/control/deviceProtocol.js +68 -0
  86. package/dist/workstation/control/enrollment.js +106 -0
  87. package/dist/workstation/control/inspect.js +71 -0
  88. package/dist/workstation/control/locks.js +110 -0
  89. package/dist/workstation/control/protocol.js +40 -0
  90. package/dist/workstation/control/restartGuard.js +46 -0
  91. package/dist/workstation/control/rootFormat.js +16 -0
  92. package/dist/workstation/control/runtimeClient.js +152 -0
  93. package/dist/workstation/control/runtimeHost.js +235 -0
  94. package/dist/workstation/control/serverIdentity.js +35 -0
  95. package/dist/workstation/control/updateTransition.js +91 -0
  96. package/dist/workstation/daemon.js +266 -264
  97. package/dist/workstation/daemonctl.js +226 -25
  98. package/dist/workstation/desktopControl/audio.js +99 -0
  99. package/dist/workstation/desktopControl/automationDevice.js +219 -0
  100. package/dist/workstation/desktopControl/automationJson.js +74 -0
  101. package/dist/workstation/desktopControl/automationLocalState.js +23 -0
  102. package/dist/workstation/desktopControl/automationProtocol.js +253 -0
  103. package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
  104. package/dist/workstation/desktopControl/automationSession.js +81 -0
  105. package/dist/workstation/desktopControl/automationTransport.js +138 -0
  106. package/dist/workstation/desktopControl/capabilities.js +93 -0
  107. package/dist/workstation/desktopControl/captureModes.js +9 -0
  108. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  109. package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
  110. package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
  111. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  112. package/dist/workstation/desktopControl/helperProcess.js +602 -0
  113. package/dist/workstation/desktopControl/hostGrant.js +65 -0
  114. package/dist/workstation/desktopControl/lobby.js +131 -0
  115. package/dist/workstation/desktopControl/macProvider.js +156 -0
  116. package/dist/workstation/desktopControl/media.js +331 -0
  117. package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
  118. package/dist/workstation/desktopControl/nativeProvider.js +1044 -0
  119. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  120. package/dist/workstation/desktopControl/overview.js +58 -0
  121. package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
  122. package/dist/workstation/desktopControl/protocol.js +311 -0
  123. package/dist/workstation/desktopControl/provider.js +1 -0
  124. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  125. package/dist/workstation/desktopControl/rateController.js +226 -0
  126. package/dist/workstation/desktopControl/supervisor.js +1774 -0
  127. package/dist/workstation/desktopControl/transport.js +27 -0
  128. package/dist/workstation/desktopControl/wallpaper.js +98 -0
  129. package/dist/workstation/exec/policy.js +5 -0
  130. package/dist/workstation/files/client.js +10 -1
  131. package/dist/workstation/files/credentials.js +142 -0
  132. package/dist/workstation/files/host.js +9 -2
  133. package/dist/workstation/files/text.js +42 -8
  134. package/dist/workstation/journal.js +5 -2
  135. package/dist/workstation/keepers/acp/client.js +38 -2
  136. package/dist/workstation/keepers/kit/buildVersion.js +35 -11
  137. package/dist/workstation/keepers/kit/bundle.js +21 -17
  138. package/dist/workstation/keepers/kit/client.js +2 -2
  139. package/dist/workstation/keepers/kit/launcher.js +123 -7
  140. package/dist/workstation/keepers/kit/paths.js +2 -1
  141. package/dist/workstation/keepers/legacy.js +16 -2
  142. package/dist/workstation/keepers/policy.js +5 -34
  143. package/dist/workstation/keepers/terminal/client.js +115 -23
  144. package/dist/workstation/keepers/terminal/exec.js +6 -3
  145. package/dist/workstation/keepers/terminal/frames.js +73 -0
  146. package/dist/workstation/keepers/terminal/generations.js +5 -8
  147. package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
  148. package/dist/workstation/keepers/terminal/input.js +146 -0
  149. package/dist/workstation/keepers/terminal/keeper.js +22 -8
  150. package/dist/workstation/keepers/terminal/main.js +1 -1
  151. package/dist/workstation/keepers/terminal/metadata.js +138 -0
  152. package/dist/workstation/keepers/terminal/protocol.js +4 -28
  153. package/dist/workstation/keepers/terminal/pty.js +1 -330
  154. package/dist/workstation/keepers/terminal/screen.js +246 -0
  155. package/dist/workstation/keepers/terminal/sessions.js +526 -0
  156. package/dist/workstation/managedInstall.js +57 -0
  157. package/dist/workstation/oauth/deviceAuthorization.js +31 -17
  158. package/dist/workstation/protocol/agent.js +2 -15
  159. package/dist/workstation/protocol/management.js +113 -5
  160. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  161. package/dist/workstation/rd/artifact.js +120 -0
  162. package/dist/workstation/rd/control.js +117 -0
  163. package/dist/workstation/rd/display.js +29 -0
  164. package/dist/workstation/rd/host.js +213 -0
  165. package/dist/workstation/rd/migrationReadiness.js +46 -0
  166. package/dist/workstation/rd/provider.js +256 -0
  167. package/dist/workstation/rd/sessions.js +391 -0
  168. package/dist/workstation/rd/state.js +57 -0
  169. package/dist/workstation/rd/update.js +168 -0
  170. package/dist/workstation/rd/upkeep.js +80 -0
  171. package/dist/workstation/register.js +27 -37
  172. package/dist/workstation/registrations/enrollment.js +250 -0
  173. package/dist/workstation/registrations/enrollmentPending.js +361 -0
  174. package/dist/workstation/registrations/migration.js +356 -0
  175. package/dist/workstation/registrations/model.js +34 -0
  176. package/dist/workstation/registrations/removal.js +222 -0
  177. package/dist/workstation/registrations/store.js +737 -0
  178. package/dist/workstation/runtime/desktopBinding.js +20 -0
  179. package/dist/workstation/runtime/deviceDesktop.js +193 -0
  180. package/dist/workstation/runtime/deviceRuntime.js +568 -0
  181. package/dist/workstation/runtime/deviceServices.js +117 -0
  182. package/dist/workstation/runtime/events.js +20 -0
  183. package/dist/workstation/runtime/ownership.js +100 -0
  184. package/dist/workstation/runtime/registrationRuntime.js +575 -0
  185. package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
  186. package/dist/workstation/runtime/removalActivity.js +184 -0
  187. package/dist/workstation/service/installer.js +197 -14
  188. package/dist/workstation/service/launchd.js +2 -0
  189. package/dist/workstation/service/systemd.js +1 -0
  190. package/dist/workstation/storageMetrics.js +118 -0
  191. package/dist/workstation/systemMetrics.js +47 -92
  192. package/dist/workstation/toolchain.js +94 -0
  193. package/dist/workstation/tools/browser.js +28 -4
  194. package/dist/workstation/tools/client.js +8 -27
  195. package/dist/workstation/tools/info.js +6 -13
  196. package/dist/workstation/tools/terminalRouter.js +17 -172
  197. package/dist/workstation/tools/validation.js +55 -1
  198. package/dist/workstation/update/archiveEvidence.js +206 -0
  199. package/dist/workstation/update/artifact.js +2 -1
  200. package/dist/workstation/update/coordinator.js +34 -13
  201. package/dist/workstation/update/deviceCompatibility.js +119 -0
  202. package/dist/workstation/update/deviceCoordinator.js +369 -0
  203. package/dist/workstation/update/deviceHealth.js +104 -0
  204. package/dist/workstation/update/deviceHealthProtocol.js +26 -0
  205. package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
  206. package/dist/workstation/update/deviceRollback.js +78 -0
  207. package/dist/workstation/update/globalInstall.js +186 -0
  208. package/dist/workstation/update/legacyBridge.js +414 -0
  209. package/dist/workstation/update/legacyOwner.js +39 -0
  210. package/dist/workstation/update/npmInstaller.js +51 -25
  211. package/dist/workstation/update/privateDurability.js +61 -0
  212. package/dist/workstation/update/rdRollback.js +145 -0
  213. package/dist/workstation/update/recoveryPlan.js +124 -0
  214. package/dist/workstation/update/runUpdate.js +10 -3
  215. package/dist/workstation/update/taskRegistry.js +161 -3
  216. package/dist/workstation/update/watchdog.js +29806 -333
  217. package/dist/workstation/update/watchdogProcess.js +72 -0
  218. package/package.json +11 -7
  219. package/skills/axom/SKILL.md +11 -11
  220. package/skills/axom-definition/SKILL.md +3 -3
  221. package/skills/axom-platform/SKILL.md +3 -3
  222. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  223. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
  224. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  225. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  226. package/dist/commands/guide.js +0 -38
  227. package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
  228. package/dist/runtime-deps/qoder-sdk/package.json +0 -8
  229. package/dist/workstation/acp/delegationMcp.js +0 -24
  230. package/dist/workstation/acp/processor/sideChannel.js +0 -80
  231. package/dist/workstation/acp/qoderStartup.js +0 -45
  232. package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
  233. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
  234. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
  235. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
  236. package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
  237. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  238. package/dist/workstation/qoderAdapter/contract.js +0 -78
  239. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
  240. package/dist/workstation/qoderAdapter/history.js +0 -85
  241. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  242. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  243. package/dist/workstation/qoderAdapter/main.js +0 -95
  244. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  245. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  246. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  247. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  248. package/dist/workstation/qoderAdapter/profile.js +0 -93
  249. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
  250. package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
  251. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
  252. package/dist/workstation/qoderAdapter/session.js +0 -319
  253. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  254. package/dist/workstation/qoderAdapter/subwork.js +0 -85
  255. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  256. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  257. package/dist/workstation/qoderAdapter/usage.js +0 -37
  258. /package/dist/workstation/{qoderAdapter/types.js → desktopControl/automationPreparation.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
+ }
@@ -0,0 +1,124 @@
1
+ import { constants, promises as fs } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import path from 'node:path';
4
+ import { z } from 'zod';
5
+ import { deviceWatchdogRecoverySchema, runtimeFilesDigest } from './deviceHealthProtocol.js';
6
+ import { readArchiveEvidence } from './archiveEvidence.js';
7
+ import { deploymentDownloadURL } from '../control/serverIdentity.js';
8
+ const absolute = z.string().min(1).max(4096).refine(value => path.isAbsolute(value) && path.resolve(value) === value);
9
+ export const deviceWatchdogPlanSchema = z.object({
10
+ schemaVersion: z.literal(2),
11
+ program: z.object({ path: absolute, sha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict(),
12
+ deviceRecovery: deviceWatchdogRecoverySchema,
13
+ rdRollback: z.object({ directory: absolute, snapshot: absolute }).strict().optional(),
14
+ commandId: z.string().uuid(), targetVersion: z.string().min(1).max(128), archive: absolute,
15
+ marker: absolute, state: absolute, npmPath: absolute, npmPrefix: absolute, cliPath: absolute,
16
+ npmEnvironment: z.object({ NPM_CONFIG_PREFIX: absolute, PATH: z.string().max(32768), NPM_CONFIG_CACHE: absolute.optional() }).strict().optional(),
17
+ }).strict();
18
+ export function updateTransactionKey(transactionId) { return createHash('sha256').update(transactionId).digest('hex'); }
19
+ export async function readPrivateUpdateBytes(file, maximum) {
20
+ const handle = await fs.open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
21
+ try {
22
+ const before = await handle.stat();
23
+ if (!before.isFile() || before.nlink !== 1 || before.size > maximum || before.mode & 0o077 || process.getuid && before.uid !== process.getuid())
24
+ throw Error('Unsafe private update evidence');
25
+ const bytes = await handle.readFile();
26
+ const after = await handle.stat();
27
+ if (bytes.length !== before.size || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs)
28
+ throw Error('Private update evidence changed while reading');
29
+ return bytes;
30
+ }
31
+ finally {
32
+ await handle.close();
33
+ }
34
+ }
35
+ export async function readDeviceWatchdogPlan(planPath, expectedRoot) {
36
+ const plan = deviceWatchdogPlanSchema.parse(JSON.parse((await readPrivateUpdateBytes(planPath, 1_048_576)).toString('utf8')));
37
+ const root = path.resolve(expectedRoot), key = updateTransactionKey(plan.commandId), update = path.join(root, 'update');
38
+ const recovery = plan.deviceRecovery;
39
+ if (recovery.root !== root || recovery.expectation.transactionId !== plan.commandId || recovery.expectation.release.version !== plan.targetVersion || recovery.rollback.archive !== plan.archive || planPath !== path.join(update, `watchdog-${key}.json`) || plan.program.path !== path.join(update, `watchdog-${key}.mjs`) || plan.archive !== path.join(update, `rollback-${key}.tgz`) || plan.marker !== path.join(update, 'restart-ready.json') || plan.state !== path.join(root, 'update-state.json') || plan.rdRollback && (plan.rdRollback.directory !== root || plan.rdRollback.snapshot !== path.join(update, `rd-rollback-${key}.json`)))
40
+ throw Error('Device watchdog plan does not bind the original transaction');
41
+ const program = await readPrivateUpdateBytes(plan.program.path, 33_554_432);
42
+ if (createHash('sha256').update(program).digest('hex') !== plan.program.sha256)
43
+ throw Error('Copied device watchdog program digest changed');
44
+ if (recovery.expectation.resolvedSource !== deploymentDownloadURL(recovery.expectation.origin.server, recovery.expectation.release.url))
45
+ throw Error('Copied watchdog source binding changed');
46
+ const source = recovery.rollback.source;
47
+ if (source.kind === 'local-pack' && (!source.packageRoot || !path.isAbsolute(source.packageRoot)))
48
+ throw Error('Original rollback package source is missing');
49
+ if (source.kind === 'authenticated-release' && (source.release.version !== recovery.rollback.version || source.release.sha512 !== recovery.rollback.sha512 || source.release.size !== recovery.rollback.size || !deploymentDownloadURL(source.server, source.release.url)))
50
+ throw Error('Original authenticated rollback source changed');
51
+ return plan;
52
+ }
53
+ export async function readDeviceBootstrapRecovery(root, startup) {
54
+ const { deviceTransactionSchema } = await import('./deviceCoordinator.js');
55
+ const transaction = deviceTransactionSchema.parse(JSON.parse((await readPrivateUpdateBytes(path.join(root, 'update/device-transaction.json'), 33_554_432)).toString('utf8')));
56
+ const device = z.object({ schemaVersion: z.literal(1), deviceId: z.string().uuid(), initialized: z.literal(true) }).strict().parse(JSON.parse((await readPrivateUpdateBytes(path.join(root, 'device.json'), 4096)).toString('utf8')));
57
+ if (!transaction.recoveryRequired && !transaction.installedRollback && transaction.installation === 'not-started' && ['BUSY', 'FAILED'].includes(transaction.phase)) {
58
+ if ((await fs.readdir(path.join(root, 'update'))).some(name => /^watchdog-.*\.json(?:\..*)?$/.test(name)))
59
+ throw Error('An armed watchdog still owns terminal recovery');
60
+ try {
61
+ const state = JSON.parse((await readPrivateUpdateBytes(path.join(root, 'update-state.json'), 65536)).toString('utf8'));
62
+ if (!['BUSY', 'FAILED', 'SUCCEEDED', 'ROLLED_BACK'].includes(state.phase))
63
+ throw Error('Public update evidence remains unresolved');
64
+ }
65
+ catch (error) {
66
+ if (error.code !== 'ENOENT')
67
+ throw error;
68
+ }
69
+ return;
70
+ }
71
+ if (!device.initialized || transaction.recoveryRequired && !transaction.installedRollback || !transaction.targetEvidence || !['RESTARTING', 'SUCCEEDED', 'ROLLED_BACK', ...(transaction.installedRollback ? ['FAILED'] : [])].includes(transaction.phase))
72
+ throw Error('Only an already initialized device can adopt its exact new recovery transaction');
73
+ if (transaction.targetEvidence.version !== transaction.release.version || transaction.targetEvidence.sha512 !== transaction.release.sha512 || transaction.targetEvidence.size !== transaction.release.size || transaction.resolvedSource !== deploymentDownloadURL(transaction.origin.server, transaction.release.url))
74
+ throw Error('Exact target archive and source evidence changed');
75
+ const state = JSON.parse((await readPrivateUpdateBytes(path.join(root, 'update-state.json'), 65536)).toString('utf8'));
76
+ if (!['RESTARTING', 'SUCCEEDED', 'ROLLED_BACK', ...(transaction.installedRollback ? ['FAILED'] : [])].includes(state.phase) || state.commandId !== transaction.transactionId || state.version !== transaction.release.version || state.releaseSha512 && state.releaseSha512 !== transaction.release.sha512)
77
+ throw Error('Device recovery state does not match the exact transaction');
78
+ if (transaction.lifecycle !== 'provision-and-arm') {
79
+ const policy = transaction.restartPolicy;
80
+ if (!policy || policy.mode !== (transaction.lifecycle === 'container' ? 'external' : 'manual') || policy.predecessor?.generation !== transaction.initiatingDaemonGeneration || !policy.predecessor.prepared || transaction.installation !== 'started' || transaction.installedRollback || !['RESTARTING', 'SUCCEEDED'].includes(transaction.phase) || !['RESTARTING', 'SUCCEEDED'].includes(state.phase))
81
+ throw Error('Explicit restart policy and positive original predecessor handoff are required');
82
+ if ((await fs.readdir(path.join(root, 'update'))).some(name => /^watchdog-.*\.json(?:\..*)?$/.test(name)))
83
+ throw Error('An armed watchdog cannot be adopted as a no-watchdog restart');
84
+ if (startup.version !== transaction.release.version || startup.runtimeDigest !== runtimeFilesDigest(transaction.targetEvidence.runtimeFiles))
85
+ throw Error('Original startup inventory differs from the exact no-watchdog restart target');
86
+ return { transaction, plan: undefined };
87
+ }
88
+ if (transaction.restartPolicy)
89
+ throw Error('Managed watchdog policy cannot be replaced by a no-watchdog declaration');
90
+ const plan = await readDeviceWatchdogPlan(path.join(root, 'update', `watchdog-${updateTransactionKey(transaction.transactionId)}.json`), root);
91
+ const expected = plan.deviceRecovery.expectation;
92
+ if (expected.transactionId !== transaction.transactionId || expected.challenge !== transaction.challenge || expected.initiatingDaemonGeneration !== transaction.initiatingDaemonGeneration || JSON.stringify(expected.origin) !== JSON.stringify(transaction.origin) || JSON.stringify(expected.release) !== JSON.stringify(transaction.release) || expected.resolvedSource !== transaction.resolvedSource || expected.runtimeDigest !== runtimeFilesDigest(transaction.targetEvidence.runtimeFiles))
93
+ throw Error('Device recovery plan changed its original private transaction');
94
+ const rollback = await readArchiveEvidence(plan.archive), frozen = plan.deviceRecovery.rollback;
95
+ if (rollback.name !== frozen.name || rollback.version !== frozen.version || rollback.size !== frozen.size || rollback.sha512 !== frozen.sha512 || JSON.stringify(rollback.reader) !== JSON.stringify(frozen.reader))
96
+ throw Error('Frozen rollback reader archive changed');
97
+ if (transaction.installedRollback) {
98
+ const installed = transaction.installedRollback;
99
+ if (installed.transactionId !== transaction.transactionId || installed.archive.version !== rollback.version || installed.archive.sha512 !== rollback.sha512 || installed.archive.size !== rollback.size || runtimeFilesDigest(installed.archive.runtimeFiles) !== runtimeFilesDigest(rollback.runtimeFiles))
100
+ throw Error('Installed rollback evidence differs from the exact frozen recovery archive');
101
+ const { assertDeviceRollbackReceipt } = await import('./deviceRollback.js');
102
+ await assertDeviceRollbackReceipt(root, expected, installed.handoff);
103
+ }
104
+ else if (state.phase === 'ROLLED_BACK')
105
+ throw Error('A terminal label does not prove installed rollback bytes');
106
+ const expectedVersion = transaction.installedRollback ? rollback.version : transaction.release.version;
107
+ const expectedDigest = transaction.installedRollback ? runtimeFilesDigest(rollback.runtimeFiles) : expected.runtimeDigest;
108
+ if (startup.version !== expectedVersion || startup.runtimeDigest !== expectedDigest)
109
+ throw Error('Original process startup inventory is not the exact recovery package');
110
+ return { transaction, plan };
111
+ }
112
+ export async function deviceRollbackCompleted(root, startup, originalTransactionId) {
113
+ const { deviceTransactionSchema } = await import('./deviceCoordinator.js');
114
+ const transaction = deviceTransactionSchema.parse(JSON.parse((await readPrivateUpdateBytes(path.join(root, 'update/device-transaction.json'), 33_554_432)).toString('utf8')));
115
+ if (transaction.transactionId !== originalTransactionId || transaction.phase !== 'ROLLED_BACK' || transaction.recoveryRequired || !transaction.targetEvidence || !transaction.installedRollback)
116
+ return false;
117
+ const installed = transaction.installedRollback, archive = await readArchiveEvidence(path.join(root, 'update', `rollback-${updateTransactionKey(originalTransactionId)}.tgz`));
118
+ if (installed.transactionId !== originalTransactionId || installed.archive.sha512 !== archive.sha512 || installed.archive.size !== archive.size || installed.archive.version !== archive.version || runtimeFilesDigest(installed.archive.runtimeFiles) !== runtimeFilesDigest(archive.runtimeFiles) || startup.version !== archive.version || startup.runtimeDigest !== runtimeFilesDigest(archive.runtimeFiles))
119
+ throw Error('Completed rollback no longer matches original startup and frozen installed evidence');
120
+ const { assertDeviceRollbackReceipt, writePrivateDeviceEvidence } = await import('./deviceRollback.js');
121
+ await assertDeviceRollbackReceipt(root, { transactionId: transaction.transactionId, challenge: transaction.challenge, initiatingDaemonGeneration: transaction.initiatingDaemonGeneration, origin: transaction.origin, release: transaction.release, resolvedSource: transaction.resolvedSource, runtimeDigest: runtimeFilesDigest(transaction.targetEvidence.runtimeFiles) }, installed.handoff);
122
+ await writePrivateDeviceEvidence(path.join(root, 'update/device-transaction.json'), transaction);
123
+ return true;
124
+ }
@@ -8,6 +8,7 @@ import { TaskRegistry } from './taskRegistry.js';
8
8
  import { LastKnownGoodCache, SERVICE_UNREGISTERED_REMEDIATION, writeReadinessMarker } from './watchdog.js';
9
9
  import { updateServiceContext } from '../config.js';
10
10
  import { CLIENT_VERSION } from '../version.js';
11
+ import { readArchiveEvidence } from './archiveEvidence.js';
11
12
  /**
12
13
  * The argv `provisionService` spawns the installed CLI with. Exported so a test can spawn the
13
14
  * same command out-of-process and prove it resolves against the unified CLI's actual command
@@ -124,7 +125,7 @@ export function lifecyclePolicy(lifecycle) {
124
125
  export function createUpdateCoordinator(deps) {
125
126
  const policy = lifecyclePolicy(deps.serviceLifecycle);
126
127
  const updateContext = updateServiceContext(deps.dir);
127
- const npm = new NpmInstaller(updateContext);
128
+ const npm = new NpmInstaller({ ...updateContext, installationLockHeld: deps.installationLockHeld });
128
129
  // This cache also resumes the restart handshake after service supervision starts a new daemon.
129
130
  const releaseCache = new LastKnownGoodCache({
130
131
  ...updateContext, runningVersion: CLIENT_VERSION, pack: (root, destination) => npm.pack(root, destination),
@@ -133,9 +134,14 @@ export function createUpdateCoordinator(deps) {
133
134
  // rollback can re-provision with it; the watchdog runs from a copy outside the package and has
134
135
  // no way to find it otherwise.
135
136
  cliPath: deps.cliPath,
137
+ deviceTransaction: deps.deviceUpdate?.currentTransaction,
138
+ sourceServer: deps.server,
136
139
  });
137
140
  return new UpdateCoordinator({
138
- tasks: deps.tasks ?? new TaskRegistry(), installer: npm, state: new UpdateStateStore(deps.dir),
141
+ tasks: deps.deviceUpdate ? undefined : deps.tasks ?? new TaskRegistry(), installer: npm, state: new UpdateStateStore(deps.dir),
142
+ deviceAdmission: deps.deviceUpdate,
143
+ verifyArtifact: deps.deviceUpdate ? async (artifact) => { await deps.deviceUpdate.recordTarget(await readArchiveEvidence(artifact.path)); } : undefined,
144
+ persistDevicePhase: deps.deviceUpdate?.recordPhase,
139
145
  drainTimeoutMs: deps.drainTimeoutMs,
140
146
  // Without an artifact source the coordinator cannot fetch the release at all: every update and
141
147
  // reinstall fails at DOWNLOADING with "requires the configured server URL". The artifact is
@@ -180,7 +186,8 @@ export function createUpdateCoordinator(deps) {
180
186
  const log = openSync(`${deps.dir}/daemon.log`, 'a');
181
187
  let child;
182
188
  try {
183
- child = spawn(process.execPath, [deps.cliPath, ...PROVISION_SERVICE_ARGV], {
189
+ child = spawn(updateContext.nodePath, [deps.cliPath, ...PROVISION_SERVICE_ARGV], {
190
+ ...(updateContext.env ? { env: updateContext.env } : {}),
184
191
  detached: true,
185
192
  stdio: ['ignore', log, log],
186
193
  });
@@ -8,8 +8,166 @@ export class TaskRegistry {
8
8
  agentOpenReservations = new Map();
9
9
  agentSessions = new Map();
10
10
  idleWaiters = new Set();
11
+ acceptedAgentWaiters = new Set();
11
12
  stopping = null;
12
- accepting = true;
13
+ baseAccepting = true;
14
+ externalClosures = 0;
15
+ lifetime = {};
16
+ lifetimeClosed = false;
17
+ accountingUnknown = false;
18
+ pendingAccepted = 0;
19
+ inFlightAccepted = 0;
20
+ reservedAccepted = 0;
21
+ accepted = new WeakMap();
22
+ externalHolds = new WeakMap();
23
+ get accepting() { return this.baseAccepting && this.externalClosures === 0 && !this.accountingUnknown && !this.lifetimeClosed; }
24
+ set accepting(open) { this.baseAccepting = open; }
25
+ holdExternalAdmission() {
26
+ if (!Number.isSafeInteger(this.externalClosures + 1)) {
27
+ this.accountingUnknown = true;
28
+ throw new Error('Admission accounting is uncertain');
29
+ }
30
+ const hold = Object.freeze({});
31
+ this.externalHolds.set(hold, true);
32
+ this.externalClosures += 1;
33
+ return hold;
34
+ }
35
+ releaseExternalAdmission(hold) {
36
+ const active = this.externalHolds.get(hold);
37
+ if (active === undefined)
38
+ throw new Error('Invalid admission hold');
39
+ if (!active)
40
+ return;
41
+ this.externalHolds.set(hold, false);
42
+ if (this.externalClosures <= 0) {
43
+ this.accountingUnknown = true;
44
+ throw new Error('Admission accounting is uncertain');
45
+ }
46
+ this.externalClosures -= 1;
47
+ }
48
+ beginAcceptedAgentWork(kind, frame, adoption) {
49
+ if (frame.t !== kind)
50
+ throw new Error('Original agent frame kind mismatch');
51
+ const durableTurn = kind === 'agent_prompt' && typeof frame.sessionRef === 'string' && this.isDurableSession(frame.sessionRef);
52
+ const conditionalAdoption = !this.accepting && !durableTurn && kind === 'agent_prompt' && typeof frame.sessionRef === 'string' && adoption !== null && typeof adoption === 'object' ? adoption : undefined;
53
+ if (!this.accepting && !((durableTurn || conditionalAdoption !== undefined) && this.externalClosures === 0 && !this.accountingUnknown && !this.lifetimeClosed))
54
+ return null;
55
+ if (!Number.isSafeInteger(this.inFlightAccepted + 1)) {
56
+ this.accountingUnknown = true;
57
+ return null;
58
+ }
59
+ const ticket = Object.freeze({});
60
+ this.accepted.set(ticket, { frame, kind, lifetime: this.lifetime, adoption: conditionalAdoption, phase: 'pending' });
61
+ this.pendingAccepted += 1;
62
+ this.inFlightAccepted += 1;
63
+ return ticket;
64
+ }
65
+ originalAccepted(ticket, kind, frame) {
66
+ const state = this.accepted.get(ticket);
67
+ if (!state || state.frame !== frame || state.kind !== kind || state.lifetime !== this.lifetime || this.lifetimeClosed || state.phase !== 'pending')
68
+ throw new Error('Invalid or expired original agent admission');
69
+ return state;
70
+ }
71
+ reserveAcceptedAgentOpen(ticket, frame) {
72
+ const state = this.originalAccepted(ticket, 'agent_open', frame);
73
+ const count = (this.agentOpenReservations.get(frame.sessionRef) ?? 0) + 1;
74
+ if (!Number.isSafeInteger(count)) {
75
+ this.accountingUnknown = true;
76
+ throw new Error('Admission accounting is uncertain');
77
+ }
78
+ this.agentOpenReservations.set(frame.sessionRef, count);
79
+ state.phase = 'reserved';
80
+ this.pendingAccepted -= 1;
81
+ this.reservedAccepted += 1;
82
+ }
83
+ consumeAcceptedAgentWork(ticket, kind, frame) {
84
+ const state = this.originalAccepted(ticket, kind, frame);
85
+ if (state.adoption) {
86
+ const sessionRef = frame.sessionRef;
87
+ const session = typeof sessionRef === 'string' ? this.agentSessions.get(sessionRef) : undefined;
88
+ if (kind !== 'agent_prompt' || !session?.durable || session.adoption !== state.adoption)
89
+ throw new Error('Original durable adoption is not established');
90
+ }
91
+ state.phase = 'consumed';
92
+ }
93
+ finishAcceptedAgentWork(ticket) {
94
+ const state = this.accepted.get(ticket);
95
+ if (!state)
96
+ throw new Error('Invalid original agent admission');
97
+ if (state.phase === 'finished')
98
+ return;
99
+ if (state.phase !== 'reserved')
100
+ this.pendingAccepted -= 1;
101
+ else
102
+ this.reservedAccepted -= 1;
103
+ this.inFlightAccepted -= 1;
104
+ state.phase = 'finished';
105
+ state.frame = undefined;
106
+ state.adoption = undefined;
107
+ if (![this.pendingAccepted, this.inFlightAccepted, this.reservedAccepted].every(count => Number.isSafeInteger(count) && count >= 0)) {
108
+ this.accountingUnknown = true;
109
+ throw new Error('Admission accounting is uncertain');
110
+ }
111
+ if (this.inFlightAccepted === 0)
112
+ for (const done of [...this.acceptedAgentWaiters])
113
+ done();
114
+ this.notifyIfIdle();
115
+ }
116
+ /** Wait for already accepted handlers, including asynchronous opens, to finish their late-stop cleanup. */
117
+ waitForAcceptedAgentWork(timeoutMs) {
118
+ if (this.accountingUnknown)
119
+ return Promise.resolve(false);
120
+ if (this.inFlightAccepted === 0)
121
+ return Promise.resolve(true);
122
+ return new Promise(resolve => {
123
+ const finish = (settled) => {
124
+ clearTimeout(timer);
125
+ this.acceptedAgentWaiters.delete(done);
126
+ resolve(settled && !this.accountingUnknown);
127
+ };
128
+ const done = () => finish(true);
129
+ const timer = setTimeout(() => finish(false), timeoutMs);
130
+ this.acceptedAgentWaiters.add(done);
131
+ });
132
+ }
133
+ closeAgentWorkLifetime() { this.lifetimeClosed = true; this.baseAccepting = false; this.lifetime = {}; }
134
+ async cancelTargetWork() {
135
+ this.closeAgentWorkLifetime();
136
+ const shells = [...this.shellTasks.entries()];
137
+ for (const [controller] of shells)
138
+ controller.abort();
139
+ let timer;
140
+ try {
141
+ await Promise.race([Promise.all(shells.map(([, completed]) => completed)), new Promise((_, reject) => { timer = setTimeout(() => reject(Error('Original shell cleanup remains pending')), 5000); })]);
142
+ }
143
+ finally {
144
+ clearTimeout(timer);
145
+ }
146
+ }
147
+ assertTargetWorkSettled() {
148
+ if (!this.lifetimeClosed || this.accountingUnknown || this.inFlightAccepted || this.agentOpenReservations.size || this.shellTasks.size)
149
+ throw Error('Original accepted work remains pending');
150
+ }
151
+ observeTargetPending() {
152
+ let reservations = 0;
153
+ for (const count of this.agentOpenReservations.values())
154
+ reservations += count;
155
+ let pending = this.inFlightAccepted + Math.max(0, reservations - this.reservedAccepted) + this.shellTasks.size;
156
+ let known = !this.accountingUnknown;
157
+ for (const session of this.agentSessions.values())
158
+ try {
159
+ if (session.busy())
160
+ pending += 1;
161
+ }
162
+ catch {
163
+ known = false;
164
+ }
165
+ if (!Number.isSafeInteger(pending) || pending < 0) {
166
+ this.accountingUnknown = true;
167
+ known = false;
168
+ }
169
+ return { pending, registered: this.agentSessions.size, known };
170
+ }
13
171
  /** Keeper-terminal execs also counted in `activeShellTasks` right now (see {@link countKeeperExec}). */
14
172
  keeperExecsCounted = 0;
15
173
  /**
@@ -167,7 +325,7 @@ export class TaskRegistry {
167
325
  * its detach (see {@link AgentSessionRegistration}).
168
326
  */
169
327
  registerAgentSession(sessionRef, close, busy, registration = {}) {
170
- this.agentSessions.set(sessionRef, { close, busy, durable: registration.durable === true });
328
+ this.agentSessions.set(sessionRef, { close, busy, durable: registration.durable === true, adoption: registration.adoption });
171
329
  }
172
330
  /**
173
331
  * 采样式判据没有"变空闲"这个事件可以挂钩,所以持有者在可能变空闲时喊一声。
@@ -214,7 +372,7 @@ export class TaskRegistry {
214
372
  tryAdmitTurn(sessionRef) {
215
373
  // Spec §8.2: a keeper-hosted session's prompt is admitted while draining — it enters keeper-acp's log first and a restart
216
374
  // continues it; a fallback session's would be cut by the restart.
217
- return this.accepting || this.agentSessions.get(sessionRef)?.durable === true;
375
+ return !this.lifetimeClosed && (this.accepting || this.agentSessions.get(sessionRef)?.durable === true && this.externalClosures === 0);
218
376
  }
219
377
  /**
220
378
  * The restart path of an update (Task 21, spec §5.5.5): every durable session is detached (its registered close), so the next daemon