@platless/axom-cli 0.6.36 → 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 (159) hide show
  1. package/README.md +7 -1
  2. package/dist/axom-patch/cli/README.md +31 -2
  3. package/dist/axom-patch/cli/account-limits/generate.py +2 -2
  4. package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
  5. package/dist/axom-patch/cli/codex-terminal-output.json +2 -2
  6. package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
  7. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +1 -1
  8. package/dist/axom-patch/cli/probes/zcode-env-account.mjs +81 -0
  9. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
  10. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +137 -41
  11. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +5 -88
  12. package/dist/axom-patch/cli/qoder-native-acp.json +23 -23
  13. package/dist/axom-patch/cli/runtime.json +3 -1
  14. package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
  15. package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
  16. package/dist/axom-patch/cli/zcode-account-limits.json +7 -7
  17. package/dist/axom-patch/cli/zcode-env-account.json +58 -0
  18. package/dist/axom-patch/cli/zcode-models.json +9 -9
  19. package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -10
  20. package/dist/cli.js +9 -11
  21. package/dist/commands/auth.js +4 -1
  22. package/dist/commands/update.js +68 -7
  23. package/dist/commands/workstation.js +94 -223
  24. package/dist/commands/workstationRegistrations.js +116 -0
  25. package/dist/credentialBroker.js +13 -2
  26. package/dist/credentials.js +16 -0
  27. package/dist/install/claude/commands.js +2 -2
  28. package/dist/keepers/terminal/keeper.mjs +1089 -673
  29. package/dist/keepers/terminal/manifest.json +4 -4
  30. package/dist/native/darwin-x64/axom-quic +0 -0
  31. package/dist/workstation/acp/acpAgents.js +11 -4
  32. package/dist/workstation/acp/outbox.js +12 -5
  33. package/dist/workstation/acp/permission.js +2 -2
  34. package/dist/workstation/acp/processor/checkpoint.js +4 -3
  35. package/dist/workstation/acp/processor/control.js +2 -2
  36. package/dist/workstation/acp/processor/jsonrpc.js +8 -0
  37. package/dist/workstation/acp/processor/session.js +3 -2
  38. package/dist/workstation/acp/processor/state.js +1 -0
  39. package/dist/workstation/acp/processor/transport/local.js +19 -3
  40. package/dist/workstation/acp/processor/updates.js +25 -0
  41. package/dist/workstation/acp/processorSession.js +12 -1
  42. package/dist/workstation/acp/registry/catalog.js +6 -6
  43. package/dist/workstation/acp/test/keeperWorld.js +4 -3
  44. package/dist/workstation/agentSessions.js +76 -23
  45. package/dist/workstation/channels/agent.js +9 -1
  46. package/dist/workstation/channels/computer.js +27 -4
  47. package/dist/workstation/channels/management.js +36 -5
  48. package/dist/workstation/config.js +18 -6
  49. package/dist/workstation/control/bindingStore.js +4 -0
  50. package/dist/workstation/control/commands.js +1 -0
  51. package/dist/workstation/control/daemonIdentity.js +90 -0
  52. package/dist/workstation/control/deviceCommands.js +191 -0
  53. package/dist/workstation/control/deviceProtocol.js +68 -0
  54. package/dist/workstation/control/locks.js +51 -4
  55. package/dist/workstation/control/rootFormat.js +16 -0
  56. package/dist/workstation/control/runtimeClient.js +60 -5
  57. package/dist/workstation/control/runtimeHost.js +96 -9
  58. package/dist/workstation/control/serverIdentity.js +2 -2
  59. package/dist/workstation/control/updateTransition.js +91 -0
  60. package/dist/workstation/daemon.js +262 -360
  61. package/dist/workstation/daemonctl.js +150 -12
  62. package/dist/workstation/desktopControl/automationDevice.js +219 -0
  63. package/dist/workstation/desktopControl/automationJson.js +74 -0
  64. package/dist/workstation/desktopControl/automationLocalState.js +23 -0
  65. package/dist/workstation/desktopControl/automationPreparation.js +1 -0
  66. package/dist/workstation/desktopControl/automationProtocol.js +253 -0
  67. package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
  68. package/dist/workstation/desktopControl/automationSession.js +81 -0
  69. package/dist/workstation/desktopControl/automationTransport.js +138 -0
  70. package/dist/workstation/desktopControl/capabilities.js +2 -1
  71. package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
  72. package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
  73. package/dist/workstation/desktopControl/deviceSockets.js +2 -2
  74. package/dist/workstation/desktopControl/helperProcess.js +250 -1
  75. package/dist/workstation/desktopControl/hostGrant.js +65 -0
  76. package/dist/workstation/desktopControl/lobby.js +4 -2
  77. package/dist/workstation/desktopControl/macProvider.js +41 -40
  78. package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
  79. package/dist/workstation/desktopControl/nativeProvider.js +31 -5
  80. package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
  81. package/dist/workstation/desktopControl/protocol.js +15 -6
  82. package/dist/workstation/desktopControl/supervisor.js +110 -17
  83. package/dist/workstation/desktopControl/wallpaper.js +26 -6
  84. package/dist/workstation/exec/policy.js +5 -0
  85. package/dist/workstation/files/client.js +10 -1
  86. package/dist/workstation/files/host.js +9 -2
  87. package/dist/workstation/journal.js +5 -2
  88. package/dist/workstation/keepers/acp/client.js +38 -2
  89. package/dist/workstation/keepers/kit/buildVersion.js +35 -11
  90. package/dist/workstation/keepers/kit/client.js +2 -2
  91. package/dist/workstation/keepers/kit/launcher.js +80 -4
  92. package/dist/workstation/keepers/legacy.js +16 -2
  93. package/dist/workstation/keepers/policy.js +5 -34
  94. package/dist/workstation/keepers/terminal/client.js +115 -23
  95. package/dist/workstation/keepers/terminal/exec.js +6 -3
  96. package/dist/workstation/keepers/terminal/frames.js +73 -0
  97. package/dist/workstation/keepers/terminal/generations.js +5 -8
  98. package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
  99. package/dist/workstation/keepers/terminal/input.js +146 -0
  100. package/dist/workstation/keepers/terminal/keeper.js +22 -8
  101. package/dist/workstation/keepers/terminal/main.js +1 -1
  102. package/dist/workstation/keepers/terminal/metadata.js +138 -0
  103. package/dist/workstation/keepers/terminal/protocol.js +4 -28
  104. package/dist/workstation/keepers/terminal/pty.js +1 -330
  105. package/dist/workstation/keepers/terminal/screen.js +246 -0
  106. package/dist/workstation/keepers/terminal/sessions.js +526 -0
  107. package/dist/workstation/oauth/deviceAuthorization.js +31 -17
  108. package/dist/workstation/protocol/management.js +23 -4
  109. package/dist/workstation/rd/artifact.js +4 -3
  110. package/dist/workstation/rd/control.js +3 -5
  111. package/dist/workstation/rd/host.js +213 -0
  112. package/dist/workstation/rd/migrationReadiness.js +46 -0
  113. package/dist/workstation/rd/provider.js +44 -10
  114. package/dist/workstation/rd/state.js +14 -2
  115. package/dist/workstation/rd/update.js +5 -8
  116. package/dist/workstation/rd/upkeep.js +44 -2
  117. package/dist/workstation/registrations/enrollment.js +250 -0
  118. package/dist/workstation/registrations/enrollmentPending.js +361 -0
  119. package/dist/workstation/registrations/migration.js +356 -0
  120. package/dist/workstation/registrations/model.js +34 -0
  121. package/dist/workstation/registrations/removal.js +222 -0
  122. package/dist/workstation/registrations/store.js +737 -0
  123. package/dist/workstation/runtime/desktopBinding.js +20 -0
  124. package/dist/workstation/runtime/deviceDesktop.js +193 -0
  125. package/dist/workstation/runtime/deviceRuntime.js +568 -0
  126. package/dist/workstation/runtime/deviceServices.js +117 -0
  127. package/dist/workstation/runtime/events.js +20 -0
  128. package/dist/workstation/runtime/ownership.js +100 -0
  129. package/dist/workstation/runtime/registrationRuntime.js +575 -0
  130. package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
  131. package/dist/workstation/runtime/removalActivity.js +184 -0
  132. package/dist/workstation/service/installer.js +93 -1
  133. package/dist/workstation/service/launchd.js +1 -0
  134. package/dist/workstation/service/systemd.js +1 -0
  135. package/dist/workstation/systemMetrics.js +3 -0
  136. package/dist/workstation/tools/client.js +8 -27
  137. package/dist/workstation/tools/info.js +6 -13
  138. package/dist/workstation/tools/terminalRouter.js +17 -172
  139. package/dist/workstation/tools/validation.js +55 -1
  140. package/dist/workstation/update/archiveEvidence.js +206 -0
  141. package/dist/workstation/update/coordinator.js +34 -13
  142. package/dist/workstation/update/deviceCompatibility.js +119 -0
  143. package/dist/workstation/update/deviceCoordinator.js +369 -0
  144. package/dist/workstation/update/deviceHealth.js +104 -0
  145. package/dist/workstation/update/deviceHealthProtocol.js +26 -0
  146. package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
  147. package/dist/workstation/update/deviceRollback.js +78 -0
  148. package/dist/workstation/update/globalInstall.js +80 -10
  149. package/dist/workstation/update/legacyBridge.js +414 -0
  150. package/dist/workstation/update/legacyOwner.js +39 -0
  151. package/dist/workstation/update/npmInstaller.js +2 -2
  152. package/dist/workstation/update/privateDurability.js +61 -0
  153. package/dist/workstation/update/recoveryPlan.js +124 -0
  154. package/dist/workstation/update/runUpdate.js +7 -1
  155. package/dist/workstation/update/taskRegistry.js +161 -3
  156. package/dist/workstation/update/watchdog.js +27427 -14410
  157. package/dist/workstation/update/watchdogProcess.js +72 -0
  158. package/package.json +7 -3
  159. package/dist/native/darwin-arm64/axom-quic +0 -0
@@ -1,40 +1,32 @@
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';
1
+ import { claimDeviceDaemonIdentity } from './control/daemonIdentity.js';
2
+ import { openDeviceRuntimeHost } from './control/runtimeHost.js';
3
+ import { applyDeviceEnrollment, applyDeviceRemoval, inspectDeviceState } from './control/deviceCommands.js';
4
+ import { RegistrationRemovalController } from './registrations/removal.js';
5
+ import { LocalControlError } from './control/protocol.js';
6
+ import { DeviceRuntime } from './runtime/deviceRuntime.js';
7
+ import { createRegistrationRuntime } from './runtime/registrationRuntime.js';
8
+ import { createDeviceServices } from './runtime/deviceServices.js';
9
+ import { readLegacyRegistrationSources } from './registrations/migration.js';
5
10
  import { promises as fs } from 'node:fs';
6
11
  import os from 'node:os';
7
12
  import path from 'node:path';
8
13
  import { fileURLToPath } from 'node:url';
9
14
  import { AGENT_WIRE_MIN_VERSION } from './protocol/agent.js';
10
- import { clientDir, loadClientConfig, updateServiceContext } from './config.js';
11
- import { AgentSessionManager } from './agentSessions.js';
12
- import { AcpAgents } from './acp/acpAgents.js';
13
- import { ConnectionSupervisor } from './connection/supervisor.js';
14
- import { ManagementChannel } from './channels/management.js';
15
- import { ComputerChannel } from './channels/computer.js';
16
- import { AgentChannel, dispatchAgentFrame as dispatchAgentChannelFrame } from './channels/agent.js';
17
- import { TaskRegistry } from './update/taskRegistry.js';
18
- import { RdUpkeep } from './rd/upkeep.js';
15
+ import { clientDir, updateServiceContext } from './config.js';
16
+ import { dispatchAgentFrame as dispatchAgentChannelFrame } from './channels/agent.js';
19
17
  import { createUpdateCoordinator } from './update/runUpdate.js';
20
- import { resolveCliPath } from './daemonctl.js';
21
- import { SystemMetricsSampler } from './systemMetrics.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';
18
+ import { CLIENT_VERSION } from './version.js';
19
+ import { captureStartupPackageEvidence, deviceHealthExpectationSchema } from './update/deviceHealth.js';
20
+ import { readDeviceBootstrapRecovery } from './update/recoveryPlan.js';
21
+ import { readLegacyBridge, releaseLegacyBridge, prepareLegacyContainerBridge, recoverLegacyContainer, legacyBridgeUpdateSnapshot, resumeLegacyManagedBridge } from './update/legacyBridge.js';
22
+ import { prepareLegacyRdReadiness } from './rd/migrationReadiness.js';
23
+ import { createDeviceRecoveryTick } from './update/deviceRecoveryLifecycle.js';
24
+ import { UpdateStateStore } from './update/stateStore.js';
25
+ import { prepareDeviceRollbackHandoff } from './update/deviceRollback.js';
26
+ import { RdUpkeep } from './rd/upkeep.js';
27
+ import { adoptBundledRd, synchronizeRd } from './rd/update.js';
28
+ import { readRdState } from './rd/state.js';
29
+ import { resolveCliPath, serviceContext } from './daemonctl.js';
38
30
  /**
39
31
  * The path `runDaemon` hands `createUpdateCoordinator` as `cliPath`, used to re-provision the OS
40
32
  * service after an update (see runUpdate.ts's `provisionService`).
@@ -240,8 +232,6 @@ export async function existingDirectories(roots) {
240
232
  }
241
233
  /** How often the daemon re-reads the workspaces config (and root existence) to restart the files host on a change. */
242
234
  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
235
  /**
246
236
  * Builds the object `ManagementChannelOptions.keepers` reports (Task 26): the policy's statuses, never a rejection (a failure is
247
237
  * `null`, which the channel also guarantees). `countedExecs` (final review minor 7): keeper-terminal execs `activeShellTasks` also
@@ -256,7 +246,7 @@ export async function buildKeeperStatuses(policy, countedExecs = () => 0) {
256
246
  catch {
257
247
  return null;
258
248
  }
259
- const terminals = statuses.filter((status) => status.kind === 'terminal' && !status.legacy);
249
+ const terminals = statuses.filter((status) => status.kind === 'terminal');
260
250
  const target = terminals.find((status) => status.replacement === 'none') ?? terminals[0];
261
251
  if (!target)
262
252
  return statuses;
@@ -287,342 +277,254 @@ export function startExclusivePoll(poll, intervalMs) {
287
277
  return () => clearInterval(timer);
288
278
  }
289
279
  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
280
  try {
307
- await runOwnedDaemon(options, shutdown, state, identity);
281
+ await runDeviceDaemon(options);
308
282
  }
309
- finally {
310
- await host.close();
283
+ catch (error) {
284
+ const bridge = await readLegacyBridge(options.directory ?? clientDir());
285
+ if (bridge?.lifecycle !== 'container')
286
+ throw error;
287
+ console.error('[device] Container migration startup failed; restoring the verified predecessor', error instanceof Error ? error.message : String(error));
288
+ await recoverLegacyContainer(bridge);
311
289
  }
312
290
  }
313
- async function runOwnedDaemon(options, shutdown, localState, identity) {
314
- // Last line of defence for the whole process. Node's default for an unhandled rejection is to
315
- // *exit*, and this daemon is the only thing keeping every session on this workstation alive —
316
- // dying because one async callback rejected is strictly worse than logging and carrying on.
317
- // Registered once, on the daemon path only (the CLI's one-shot commands keep the default).
318
- process.on('unhandledRejection', (reason) => {
319
- const detail = reason instanceof Error ? (reason.stack ?? reason.message) : String(reason);
320
- console.error(`[daemon] unhandled rejection (ignored, daemon stays up): ${detail}`);
321
- });
322
- const dir = clientDir();
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;
342
- // Repair the first fixed release even when it was installed by an older ignore-scripts updater,
343
- // before a newly started keeper-terminal snapshots native dependency availability.
344
- await preflightOptionalPty(updateServiceContext(dir));
345
- const tasks = new TaskRegistry();
346
- // Attaches both the server-driven update path (onUpdate below) and axom update's manual path to
347
- // the same coordinator assembly — see runUpdate.ts.
348
- const updates = createUpdateCoordinator({
349
- dir, server: cfg.server, tasks,
350
- cliPath: daemonCliPath(),
351
- // launchd/systemd is configured with KeepAlive/Restart=always, and a container has its own
352
- // restart policy. Exiting is therefore the atomic hand-off from the old package to the freshly
353
- // installed daemon.
354
- //
355
- // Passed unconditionally on purpose: `createUpdateCoordinator` drops it for the one lifecycle
356
- // that has nothing to hand off to (`'none'` — see `lifecyclePolicy().restartsOnHandoff`),
357
- // alongside the other three policy decisions. Gating it here instead would put a fourth copy of
358
- // the same string comparison at a call site, which is exactly where the first three were
359
- // deliberately taken out of.
360
- restart: async () => { process.exit(0); },
361
- // The daemon is the service manager's own supervised process: it is the one caller that must
362
- // rewrite the service definition and arm the rollback watchdog. See RunDaemonOptions and
363
- // resolveServiceLifecycle above for how that treatment is keyed on *how this process was
364
- // invoked*, not on what the host merely supports.
365
- serviceLifecycle: resolveServiceLifecycle(options),
366
- });
367
- localState.update = updates;
368
- // One ACP client for every external agent; the roster it reports is the ACP agent registry, keyed
369
- // by registry agent id (ruling R2) — the same key `agent_open.bridge` now carries.
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();
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) });
384
- let supervisor;
385
- // Plaintext WebSockets are only for an explicitly opted-in local development fixture. Production
386
- // ticket handshakes must be WSS because the ticket is the data-channel key derivation material.
387
- const allowInsecureWs = process.env.AXOM_WORKSTATION_ALLOW_INSECURE_WS === '1';
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 () => {
291
+ async function runDeviceDaemon(options) {
292
+ const directory = options.directory ?? clientDir();
293
+ const lifecycle = resolveServiceLifecycle(options);
294
+ if (lifecycle === 'container')
295
+ await prepareLegacyContainerBridge(directory, { ...serviceContext(), configDir: directory });
296
+ const packageRoot = updateServiceContext(directory).packageRoot;
297
+ const startupPackage = await captureStartupPackageEvidence(packageRoot, CLIENT_VERSION);
298
+ const legacyBridge = await readLegacyBridge(directory, startupPackage);
299
+ if (legacyBridge?.lifecycle === 'managed')
300
+ await resumeLegacyManagedBridge(directory, startupPackage);
301
+ let legacyStartupHeld = !!legacyBridge;
302
+ if (options.desktopProvider || options.onDesktopCommand)
303
+ throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Device remote desktop integration is unavailable until the physical isolation contract is integrated');
304
+ const shutdown = new AbortController();
305
+ const services = options.deviceServices ?? createDeviceServices({ root: directory });
306
+ const previousUpdateSnapshot = services.updates.snapshot.bind(services.updates);
307
+ services.updates.snapshot = async (registrationId) => await legacyBridgeUpdateSnapshot(directory) ?? previousUpdateSnapshot(registrationId);
308
+ let device;
309
+ let removal;
310
+ let generation = '';
311
+ let fullyInitialized = false;
312
+ let recoveryLease;
313
+ let rollbackLease;
314
+ let rollbackPreparation;
315
+ let rollbackBinding;
316
+ let rdUpkeep;
317
+ let stopRdPoll = () => { };
318
+ const mode = options.supervisedByServiceManager ? 'launchd' : 'foreground';
319
+ const snapshot = async () => ({ ...await inspectDeviceState({ store, device, generation, mode }), desktop: services.desktopFacts?.() });
320
+ let store;
321
+ let initialized;
322
+ let initializationFailed;
323
+ const initialization = new Promise((resolve, reject) => { initialized = resolve; initializationFailed = reject; });
324
+ void initialization.catch(() => { });
325
+ let preparing;
326
+ let preparedMode;
327
+ const prepareStop = (stopMode) => {
328
+ if (preparedMode)
329
+ return preparedMode === 'terminate' || preparedMode === stopMode ? Promise.resolve() : Promise.reject(new LocalControlError('OUTCOME_UNKNOWN', 'Detached work requires verified termination before the device can be stopped'));
330
+ if (preparing)
331
+ return preparing.then(() => prepareStop(stopMode));
332
+ const retiring = device?.stop(stopMode);
333
+ void retiring?.catch(() => { });
334
+ preparing = (async () => {
335
+ await initialization;
336
+ if (retiring)
337
+ await retiring;
338
+ else
339
+ await device?.stop(stopMode);
340
+ preparedMode = stopMode;
341
+ })().catch(error => { preparing = undefined; throw error; });
342
+ return preparing;
343
+ };
344
+ let stopping;
345
+ const stop = (stopMode) => {
346
+ fullyInitialized = false;
347
+ stopping ??= (async () => {
348
+ stopRdPoll();
349
+ await rdUpkeep?.stop();
350
+ await prepareStop(stopMode);
351
+ await services.stop();
352
+ shutdown.abort();
353
+ })().catch(error => { stopping = undefined; throw error; });
354
+ return stopping;
355
+ };
356
+ const host = await claimDeviceDaemonIdentity(directory, async (identity) => {
357
+ store = identity.store;
358
+ return openDeviceRuntimeHost({ directory, mode, snapshot,
359
+ shutdown: stop,
360
+ prepareShutdown: prepareStop,
361
+ addRegistration: async (request) => {
362
+ if (!fullyInitialized || !device || preparing || stopping)
363
+ throw new LocalControlError('BUSY', 'Device lifecycle transition is in progress');
364
+ const finishMutation = device.beginMembershipMutation();
365
+ let result;
366
+ try {
367
+ result = await applyDeviceEnrollment(request, directory, snapshot, () => device.assertMutationAllowed());
368
+ }
369
+ finally {
370
+ finishMutation();
371
+ }
372
+ await device.reconcile();
373
+ return result;
374
+ },
375
+ removeRegistration: async (request) => {
376
+ if (!fullyInitialized || !removal || preparing || stopping)
377
+ throw new LocalControlError('BUSY', 'Device lifecycle transition is in progress');
378
+ return applyDeviceRemoval(request, { controller: removal, snapshot });
379
+ },
380
+ reconcile: async () => { if (!fullyInitialized || !device)
381
+ throw new LocalControlError('BUSY', 'Device initialization is in progress'); await device.reconcile(); },
382
+ update: async (release, operationId, source) => { if (!fullyInitialized)
383
+ throw new LocalControlError('BUSY', 'Device initialization is in progress'); return services.updates.local(operationId, release, source); },
384
+ updateHealth: (request, currentGeneration) => services.updates.health(request, currentGeneration),
385
+ prepareUpdateRollback: async (request, currentGeneration) => {
386
+ if (!fullyInitialized || !device)
387
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Original target initialization is incomplete');
388
+ const binding = JSON.stringify(deviceHealthExpectationSchema.parse(request));
389
+ if (rollbackBinding && rollbackBinding !== binding)
390
+ throw new LocalControlError('BINDING_CONFLICT', 'Original rollback preparation request changed');
391
+ rollbackBinding = binding;
392
+ rollbackPreparation ??= prepareDeviceRollbackHandoff({ root: directory, generation: currentGeneration, startup: startupPackage,
393
+ lease: () => { rollbackLease ??= recoveryLease ?? device.acquireMaintenance({ transactionId: request.transactionId }); return rollbackLease; },
394
+ }, request).catch(error => { rollbackPreparation = undefined; throw error; });
395
+ return rollbackPreparation;
396
+ },
397
+ });
398
+ }, startupPackage);
399
+ generation = host.generation;
400
+ let stopPoll = () => { };
401
+ const signalStop = () => { void stop('detach').catch(error => console.error('[device] shutdown needs verification', error)); };
402
+ const rejection = (reason) => console.error('[device] unhandled rejection', reason instanceof Error ? reason.message : 'unknown');
403
+ process.on('unhandledRejection', rejection);
404
+ process.once('SIGINT', signalStop);
405
+ process.once('SIGTERM', signalStop);
406
+ try {
407
+ let bootstrap;
403
408
  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();
409
+ bootstrap = await readDeviceBootstrapRecovery(directory, startupPackage);
408
410
  }
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;
411
+ catch (error) {
412
+ if (error.code !== 'ENOENT')
413
+ throw error;
414
+ }
415
+ if (legacyBridge && process.platform === 'linux') {
416
+ try {
417
+ const readiness = await prepareLegacyRdReadiness(directory);
418
+ if (readiness.kind === 'unavailable')
419
+ console.warn(`[rd] ${readiness.reason}`);
420
+ else if (readiness.kind === 'staged')
421
+ console.warn('[rd] Compatible desktop runtime staged; activation is deferred until the original desktop authority is retired and physically accepted.');
422
+ }
423
+ catch (error) {
424
+ console.warn('[rd] Legacy desktop configuration remains unavailable', error instanceof Error ? error.message : String(error));
425
+ }
426
+ }
427
+ await services.start();
428
+ device = new DeviceRuntime({ store, desktopActivity: () => services.desktopActivity?.() ?? { known: false, consumers: 1 }, factory: async (identity) => createRegistrationRuntime({ ...identity,
429
+ registrationRoot: store.root,
430
+ startupHeld: legacyStartupHeld || !!bootstrap && (bootstrap.transaction.phase === 'RESTARTING' || !!bootstrap.transaction.installedRollback && !!bootstrap.transaction.recoveryRequired),
431
+ paths: { legacySources: await readLegacyRegistrationSources(directory, identity.registration.id) }, deviceServices: services,
432
+ }) });
433
+ services.updates.configure({ directory, device, generation, lifecycle, startupPackage, packageRoot, initialized: () => fullyInitialized,
434
+ execute: (command, context) => createUpdateCoordinator({ dir: directory, server: context.source, cliPath: daemonCliPath(), serviceLifecycle: lifecycle, deviceUpdate: context, restart: async () => { await stop('detach'); } }).handle(command),
435
+ completeRecovery: async (persist) => {
436
+ if (!recoveryLease) {
437
+ await persist();
438
+ return;
439
+ }
440
+ const original = recoveryLease;
441
+ await original.release(async () => {
442
+ const recovery = await readDeviceBootstrapRecovery(directory, startupPackage);
443
+ if (!recovery)
444
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Original device restart recovery evidence is missing');
445
+ await new UpdateStateStore(directory).save({ phase: 'SUCCEEDED', commandId: recovery.transaction.transactionId, version: recovery.transaction.release.version, releaseSha512: recovery.transaction.release.sha512, remoteForcePolicy: 'drain-only' });
446
+ await persist();
447
+ });
448
+ if (recoveryLease === original)
449
+ recoveryLease = undefined;
461
450
  },
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),
496
- });
497
- const metrics = new SystemMetricsSampler(cfg.workspaces ?? []);
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) : () => { };
523
- const management = new ManagementChannel({
524
- server: cfg.server, workstationId: cfg.workstationId, workstation: cfg.workstationId, clientKey, serverPub,
525
- bridges: () => bridges,
526
- workspaces: cfg.workspaces,
527
- playgroundCwd,
528
- metrics: () => metrics.snapshot(),
529
- toolchain: () => toolchain.current(),
530
- allowInsecureWs,
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
- },
547
- onUpdate: (command) => updates.handle(command),
548
- onExternalAgentsScan: async () => {
549
- bridges = await scanRoster();
550
- return bridges;
551
- },
552
- snapshot: () => buildManagementSnapshot(supervisor, tasks, updates),
553
- keepers: () => buildKeeperStatuses(keeperPolicy, () => tasks.countedKeeperExecs),
554
- onKeeperControl: (frame) => keeperPolicy.control(frame),
555
- });
556
- options.onDesktopManagement?.(management);
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)
451
+ });
452
+ removal = new RegistrationRemovalController({ store, device, generation });
453
+ const finishInitialization = device.beginMembershipMutation();
454
+ try {
455
+ await device.reconcile();
456
+ }
457
+ finally {
458
+ finishInitialization();
459
+ }
460
+ let legacyLease;
461
+ if (legacyBridge) {
462
+ legacyLease = device.acquireMaintenance({ transactionId: legacyBridge.commandId });
463
+ await legacyLease.ready;
464
+ }
465
+ if (bootstrap && (bootstrap.transaction.phase === 'RESTARTING' || bootstrap.transaction.installedRollback && bootstrap.transaction.recoveryRequired)) {
466
+ recoveryLease = device.acquireMaintenance({ transactionId: bootstrap.transaction.transactionId });
467
+ await recoveryLease.ready;
468
+ }
469
+ const rdSource = async () => (await readRdState(directory))?.artifactSource ?? services.rdSource?.();
470
+ let pinnedRdSource;
471
+ rdUpkeep = new RdUpkeep({ device,
472
+ adopt: async () => { pinnedRdSource = await rdSource(); if (pinnedRdSource)
473
+ await adoptBundledRd(directory, pinnedRdSource); },
474
+ synchronize: async () => pinnedRdSource ? synchronizeRd(directory, pinnedRdSource, { deferActivation: true }) : undefined,
475
+ whenIdle: async (operation) => { await operation(); return true; }, refresh: () => services.publishToolState?.(), log: message => console.warn(message),
476
+ });
477
+ const tickRd = async () => { if (process.platform === 'linux' && await rdSource())
478
+ await rdUpkeep.tick(); };
479
+ await tickRd();
480
+ fullyInitialized = true;
481
+ initialized();
482
+ if (stopping)
483
+ await stopping.catch(() => { });
484
+ if (shutdown.signal.aborted)
574
485
  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)}`);
486
+ stopRdPoll = startExclusivePoll(tickRd, 30_000);
487
+ const recoveryTick = createDeviceRecoveryTick({ root: directory, startup: startupPackage, generation, bootstrap, signal: shutdown.signal,
488
+ initialized: () => fullyInitialized, lease: () => recoveryLease,
489
+ released: original => { if (recoveryLease === original)
490
+ recoveryLease = undefined; }, reconcile: () => device.reconcile(),
582
491
  });
583
- });
584
- supervisor = new ConnectionSupervisor({
585
- management, computer, agent,
586
- // Timestamped here rather than inside the supervisor: the daemon owns this log's shape. The
587
- // stamp is not decoration -- when the 2026-09-03 outage was diagnosed, the moment the tunnel
588
- // died had to be recovered from the log *file's* mtime, because not one line carried a time.
589
- log: (message) => console.log(`${new Date().toISOString()} ${message}`),
590
- });
591
- const stopIdleSweep = startIdleSweep(agents);
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
- };
601
- const stop = () => shutdown.abort();
602
- process.once('SIGINT', stop);
603
- process.once('SIGTERM', stop);
604
- try {
605
- await supervisor.run(shutdown.signal);
492
+ stopPoll = startExclusivePoll(async () => {
493
+ if (legacyLease && await releaseLegacyBridge(directory, startupPackage, generation, complete => legacyLease.release(complete))) {
494
+ legacyLease = undefined;
495
+ legacyStartupHeld = false;
496
+ await device.reconcile();
497
+ }
498
+ await recoveryTick();
499
+ }, 2_000);
500
+ if (!shutdown.signal.aborted)
501
+ await new Promise(resolve => shutdown.signal.addEventListener('abort', () => resolve(), { once: true }));
502
+ }
503
+ catch (error) {
504
+ initializationFailed(error);
505
+ throw error;
606
506
  }
607
507
  finally {
608
- stopRdUpkeep();
609
- rdUpkeep?.stop();
610
- await desktopSupervisor?.close();
611
- process.removeListener('SIGINT', stop);
612
- process.removeListener('SIGTERM', stop);
613
- stopIdleSweep();
614
- stopSwitchUpkeep();
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();
508
+ stopRdPoll();
509
+ stopPoll();
510
+ process.removeListener('SIGINT', signalStop);
511
+ process.removeListener('SIGTERM', signalStop);
512
+ process.removeListener('unhandledRejection', rejection);
513
+ try {
514
+ await rdUpkeep?.stop();
515
+ }
516
+ finally {
517
+ try {
518
+ await device?.stop('detach');
519
+ }
520
+ finally {
521
+ try {
522
+ await services.stop();
523
+ }
524
+ finally {
525
+ await host.close();
526
+ }
527
+ }
528
+ }
627
529
  }
628
530
  }