@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
@@ -1,12 +1,28 @@
1
1
  import os from 'node:os';
2
+ import { BindingStore } from './control/bindingStore.js';
3
+ import { inspectRuntime, stopRuntime } from './control/runtimeClient.js';
4
+ import { LocalControlError } from './control/protocol.js';
5
+ import { normalizeWorkstationServer } from './control/serverIdentity.js';
2
6
  import path from 'node:path';
3
7
  import { fileURLToPath } from 'node:url';
4
- import { clientDir, loadClientConfig } from './config.js';
8
+ import { randomUUID } from 'node:crypto';
9
+ import { clientDir, loadClientConfig, updateServiceContext } from './config.js';
5
10
  import { createServiceManager } from './service/installer.js';
6
11
  import { buildServicePath } from './service/types.js';
7
12
  import { loginShellPath } from './service/loginShell.js';
8
13
  import { KeeperLauncher } from './keepers/kit/launcher.js';
9
14
  import { stopKeepers } from './keepers/stop.js';
15
+ import { assertRuntimeRestartSafe } from './control/restartGuard.js';
16
+ import { promises as fs } from 'node:fs';
17
+ import { inspectDeviceRuntime, prepareDeviceRuntimeStop, stopDeviceRuntime } from './control/runtimeClient.js';
18
+ import { assertDeviceUpdateTransitionSafe } from './control/daemonIdentity.js';
19
+ import { RegistrationStore } from './registrations/store.js';
20
+ import { readLegacyRegistrationSources, assertRegistrationEnrollmentAdmission } from './registrations/migration.js';
21
+ import { assertDeviceGuard } from './control/deviceProtocol.js';
22
+ import { resolveRegistrationOwnership } from './runtime/ownership.js';
23
+ import { captureStartupPackageEvidence } from './update/deviceHealth.js';
24
+ import { CLIENT_VERSION } from './version.js';
25
+ import { readDeviceBootstrapRecovery } from './update/recoveryPlan.js';
10
26
  const __filename = fileURLToPath(import.meta.url);
11
27
  /**
12
28
  * The package's real npm bin entry (`package.json`'s `bin.axom`), given the directory a compiled
@@ -36,21 +52,27 @@ export function serviceContext() {
36
52
  throw new Error('Axomwork Workstation user service supervision is supported only on macOS and Linux.');
37
53
  }
38
54
  const directory = path.dirname(__filename);
39
- const nodeDirectory = path.dirname(process.execPath);
55
+ const managed = updateServiceContext();
56
+ const nodeDirectory = path.dirname(managed.nodePath);
40
57
  return {
41
58
  platform: process.platform,
42
59
  username: os.userInfo().username,
43
60
  userId: process.getuid?.() ?? os.userInfo().uid,
44
61
  homeDir: os.homedir(),
45
- nodePath: process.execPath,
62
+ nodePath: managed.nodePath,
46
63
  cliPath: CLI_PATH,
47
- npmPath: process.env.AXOM_WORKSTATION_NPM_PATH ?? path.join(nodeDirectory, 'npm'),
64
+ npmPath: managed.npmPath,
48
65
  // dist/workstation/ is nested under lib/node_modules/@platless/axom-cli; climb to npm's prefix.
49
- npmPrefix: process.env.AXOM_WORKSTATION_NPM_PREFIX ?? path.resolve(directory, '../../../../../..'),
66
+ npmPrefix: managed.npmPrefix,
50
67
  configDir: clientDir(),
51
- pathEnv: buildServicePath(process.env.PATH, nodeDirectory),
68
+ pathEnv: buildServicePath(managed.env?.PATH ?? process.env.PATH, nodeDirectory),
69
+ ...(managed.managed ? { npmConfigPrefix: managed.managed.prefix } : {}),
52
70
  };
53
71
  }
72
+ async function installationServiceContext(base, shellPath = loginShellPath) {
73
+ const loginPath = await shellPath();
74
+ return loginPath ? { ...base, pathEnv: buildServicePath(`${loginPath}:${process.env.PATH ?? ''}`, path.dirname(base.nodePath)) } : base;
75
+ }
54
76
  /** The workstation's keepers stopped (see {@link DaemonControllerDeps.stopKeepers}); an unregistered client has none. */
55
77
  async function stopRegisteredKeepers() {
56
78
  const configDir = clientDir();
@@ -62,6 +84,10 @@ async function stopRegisteredKeepers() {
62
84
  });
63
85
  }
64
86
  const defaultDeps = {
87
+ identity: () => new BindingStore(clientDir()).inspect(),
88
+ runtime: async () => (await (await import('./control/inspect.js')).inspectWorkstation(clientDir())).runtime,
89
+ shutdown: generation => stopRuntime(clientDir(), generation),
90
+ restartSafe: generation => assertRuntimeRestartSafe(clientDir(), generation),
65
91
  service: (context) => createServiceManager(context ?? serviceContext()),
66
92
  config: () => loadClientConfig(clientDir()),
67
93
  stopKeepers: stopRegisteredKeepers,
@@ -72,6 +98,22 @@ const defaultDeps = {
72
98
  */
73
99
  export function createDaemonController(overrides = {}) {
74
100
  const deps = { ...defaultDeps, ...overrides };
101
+ async function checked(options) {
102
+ const runtime = await deps.runtime();
103
+ if (runtime.state === 'unknown')
104
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Cannot verify the running workstation');
105
+ if (options) {
106
+ const identity = await deps.identity();
107
+ if (identity.revision !== options.guard.revision || runtime.generation !== options.guard.runtimeGeneration)
108
+ throw new LocalControlError('BINDING_CONFLICT', 'Workstation changed; check it again');
109
+ }
110
+ if (runtime.state === 'running') {
111
+ const config = await deps.config();
112
+ if (!runtime.binding || runtime.binding.server !== normalizeWorkstationServer(config.server) || runtime.binding.workstationId !== config.workstationId)
113
+ throw new LocalControlError('INSTANCE_OCCUPIED', 'Another workstation binding is running');
114
+ }
115
+ return runtime;
116
+ }
75
117
  return {
76
118
  async install() {
77
119
  // The definition is written from the login shell's PATH when one can be had, unioned with
@@ -79,11 +121,7 @@ export function createDaemonController(overrides = {}) {
79
121
  // itself after an update they are not, and only the login shell knows where the user's CLIs
80
122
  // are — see loginShellPath for why reusing the service's own PATH would make a machine
81
123
  // reinstall its own blind spot.
82
- const shellPath = await loginShellPath();
83
- const base = serviceContext();
84
- const context = shellPath
85
- ? { ...base, pathEnv: buildServicePath(`${shellPath}:${process.env.PATH ?? ''}`, path.dirname(base.nodePath)) }
86
- : base;
124
+ const context = await installationServiceContext(serviceContext());
87
125
  await deps.service(context).install({ interactive: true });
88
126
  },
89
127
  // The workstation is stopped by the user: what its keepers host ends too (the daemon first, so nothing relaunches them).
@@ -94,28 +132,191 @@ export function createDaemonController(overrides = {}) {
94
132
  finally {
95
133
  await deps.stopKeepers();
96
134
  } },
97
- async start() { await deps.service().start(); },
98
- async stop() { try {
99
- await deps.service().stop();
135
+ async start(options) { if ((await checked(options)).state === 'running')
136
+ return; await deps.service().start(); },
137
+ async stop(options) { const runtime = await checked(options); try {
138
+ if (runtime.mode === 'foreground' && runtime.generation)
139
+ await deps.shutdown(runtime.generation);
140
+ else
141
+ await deps.service().stop();
100
142
  }
101
143
  finally {
102
144
  await deps.stopKeepers();
103
145
  } },
104
146
  // A restart keeps the keepers: terminals, commands and agent sessions go on under the next daemon.
105
- async restart() { await deps.service().restart(); },
147
+ async restart(options) {
148
+ const runtime = await checked(options);
149
+ if (runtime.state === 'running' && runtime.generation)
150
+ await deps.restartSafe(runtime.generation);
151
+ if (runtime.mode === 'foreground' && runtime.generation)
152
+ await deps.shutdown(runtime.generation);
153
+ await deps.service().restart();
154
+ },
155
+ async setEnabled(enabled, options) {
156
+ const runtime = await checked(options);
157
+ const service = deps.service();
158
+ if (!service.setEnabled)
159
+ throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Service enablement is unavailable');
160
+ if (enabled)
161
+ await service.setEnabled(true);
162
+ else {
163
+ try {
164
+ await service.setEnabled(false);
165
+ if (runtime.mode === 'foreground' && runtime.generation) {
166
+ const current = await deps.runtime();
167
+ if (current.state === 'running' && current.generation === runtime.generation)
168
+ await deps.shutdown(runtime.generation);
169
+ else if (current.state !== 'stopped')
170
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'The running workstation changed during shutdown');
171
+ }
172
+ }
173
+ finally {
174
+ await deps.stopKeepers();
175
+ }
176
+ }
177
+ },
106
178
  async status() {
179
+ const runtime = await deps.runtime();
180
+ if (runtime.state === 'running' && runtime.binding)
181
+ return { running: true, server: runtime.binding.server, workstationId: runtime.binding.workstationId };
182
+ if (runtime.state === 'unknown')
183
+ return { running: false, uncertain: true };
107
184
  const state = await deps.service().status();
108
- if (!state.running)
109
- return { running: false };
110
- const config = await deps.config().catch(() => null);
111
- return { running: true, server: config?.server, workstationId: config?.workstationId };
185
+ return state.running ? { running: true, uncertain: true } : { running: false };
186
+ },
187
+ };
188
+ }
189
+ export function createDeviceDaemonController(directory, overrides = {}) {
190
+ const context = overrides.context ?? (() => ({ ...serviceContext(), configDir: directory }));
191
+ const service = overrides.service ?? ((input) => createServiceManager(input ?? context()));
192
+ const store = new RegistrationStore(directory);
193
+ const runtime = async () => {
194
+ try {
195
+ return (await inspectDeviceRuntime(directory)).runtime;
196
+ }
197
+ catch {
198
+ const legacy = await inspectRuntime(directory);
199
+ if (legacy.state !== 'stopped')
200
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'The existing owner is not a verified device daemon');
201
+ return { state: 'stopped', generation: null, mode: 'unknown' };
202
+ }
203
+ };
204
+ const checked = async (options) => {
205
+ if (options) {
206
+ const inspection = await inspectDeviceRuntime(directory);
207
+ assertDeviceGuard(options.guard, inspection);
208
+ return inspection.runtime;
209
+ }
210
+ return runtime();
211
+ };
212
+ const terminateKeepers = async () => {
213
+ const inspection = await store.inspect();
214
+ if (!inspection.known || inspection.entries.some(entry => entry.state !== 'ready'))
215
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Cannot verify every owned keeper namespace');
216
+ for (const registration of await store.list()) {
217
+ const sources = await readLegacyRegistrationSources(directory, registration.id);
218
+ const ownership = await resolveRegistrationOwnership(registration, sources);
219
+ const launcher = new KeeperLauncher({ configDir: registration.keeperConfigDirectory, workstationId: registration.workstationId, server: sources?.keeperServer ?? registration.server, ownership });
220
+ const reports = await stopKeepers(launcher);
221
+ if (reports.some(report => !report.result.stopped))
222
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'A workstation keeper has not stopped');
223
+ }
224
+ };
225
+ const finishPrepared = async (generation, mode) => {
226
+ const current = await runtime();
227
+ if (current.state === 'stopped')
228
+ return;
229
+ if (current.generation !== generation)
230
+ throw new LocalControlError('BINDING_CONFLICT', 'A successor device daemon replaced the prepared owner');
231
+ await stopDeviceRuntime(directory, generation, mode);
232
+ };
233
+ return {
234
+ async install(options) {
235
+ const installation = await installationServiceContext(context(), overrides.loginShellPath);
236
+ if (!path.isAbsolute(installation.cliPath) || path.resolve(installation.cliPath) !== installation.cliPath || path.basename(installation.cliPath) !== 'cli.js' || path.basename(path.dirname(installation.cliPath)) !== 'dist')
237
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Installed service entrypoint must identify its exact dist/cli.js package');
238
+ const startup = await captureStartupPackageEvidence(path.dirname(path.dirname(installation.cliPath)), CLIENT_VERSION);
239
+ await assertDeviceUpdateTransitionSafe(directory, startup);
240
+ if (options) {
241
+ const recovery = await readDeviceBootstrapRecovery(directory, startup);
242
+ const transaction = recovery?.transaction;
243
+ if (!transaction || transaction.lifecycle !== 'provision-and-arm' || transaction.phase !== 'RESTARTING' || transaction.installation !== 'started' || transaction.installedRollback || transaction.recoveryRequired || transaction.initiatingDaemonGeneration !== options.updateGeneration)
244
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Service replacement requires the original active device update handoff');
245
+ const current = await inspectDeviceRuntime(directory);
246
+ if (current.runtime.state !== 'running' || current.runtime.generation !== options.updateGeneration)
247
+ throw new LocalControlError('BINDING_CONFLICT', 'The original device update owner changed before service replacement');
248
+ }
249
+ await service(installation).install({ interactive: true });
112
250
  },
251
+ async start(options) { if ((await checked(options)).state === 'running')
252
+ return; await assertDeviceUpdateTransitionSafe(directory); await assertRegistrationEnrollmentAdmission(directory); await service().start(); },
253
+ async stop(options) {
254
+ const current = await checked(options);
255
+ const manager = service();
256
+ if (current.generation && current.mode === 'launchd') {
257
+ await prepareDeviceRuntimeStop(directory, current.generation, 'terminate', options?.operationId ?? randomUUID());
258
+ await manager.stop();
259
+ await finishPrepared(current.generation, 'terminate');
260
+ }
261
+ else {
262
+ if (current.generation)
263
+ await stopDeviceRuntime(directory, current.generation, 'terminate');
264
+ await manager.stop();
265
+ }
266
+ if (!current.generation)
267
+ await terminateKeepers();
268
+ },
269
+ async uninstall(options) {
270
+ const current = await checked(options);
271
+ const manager = service();
272
+ if (current.generation && current.mode === 'launchd') {
273
+ await prepareDeviceRuntimeStop(directory, current.generation, 'terminate', options?.operationId ?? randomUUID());
274
+ await manager.uninstall();
275
+ await finishPrepared(current.generation, 'terminate');
276
+ }
277
+ else {
278
+ if (current.generation)
279
+ await stopDeviceRuntime(directory, current.generation, 'terminate');
280
+ await manager.uninstall();
281
+ }
282
+ if (!current.generation)
283
+ await terminateKeepers();
284
+ },
285
+ async restart(options) {
286
+ const current = await checked(options);
287
+ const manager = service();
288
+ if (current.generation) {
289
+ await (overrides.restartSafe ?? (generation => assertRuntimeRestartSafe(directory, generation)))(current.generation);
290
+ if (current.mode === 'launchd') {
291
+ await prepareDeviceRuntimeStop(directory, current.generation, 'detach', options?.operationId ?? randomUUID());
292
+ await manager.stop();
293
+ await finishPrepared(current.generation, 'detach');
294
+ await manager.restart();
295
+ return;
296
+ }
297
+ await stopDeviceRuntime(directory, current.generation, 'detach');
298
+ }
299
+ await manager.restart();
300
+ },
301
+ async status() { const current = await runtime(); return { running: current.state === 'running' }; },
113
302
  };
114
303
  }
304
+ async function deviceControllerSelected() {
305
+ for (const name of ['device.json', 'legacy-migration.json'])
306
+ try {
307
+ await fs.lstat(path.join(clientDir(), name));
308
+ return true;
309
+ }
310
+ catch (error) {
311
+ if (error.code !== 'ENOENT')
312
+ throw error;
313
+ }
314
+ return false;
315
+ }
115
316
  const controller = createDaemonController();
116
- export const install = () => controller.install();
117
- export const uninstall = () => controller.uninstall();
118
- export const start = () => controller.start();
119
- export const stop = () => controller.stop();
120
- export const restart = () => controller.restart();
121
- export const status = () => controller.status();
317
+ export const install = async () => (await deviceControllerSelected() ? createDeviceDaemonController(clientDir()) : controller).install();
318
+ export const uninstall = async () => (await deviceControllerSelected() ? createDeviceDaemonController(clientDir()) : controller).uninstall();
319
+ export const start = async () => (await deviceControllerSelected() ? createDeviceDaemonController(clientDir()) : controller).start();
320
+ export const stop = async () => (await deviceControllerSelected() ? createDeviceDaemonController(clientDir()) : controller).stop();
321
+ export const restart = async () => (await deviceControllerSelected() ? createDeviceDaemonController(clientDir()) : controller).restart();
322
+ export const status = async () => (await deviceControllerSelected() ? createDeviceDaemonController(clientDir()) : controller).status();
@@ -0,0 +1,99 @@
1
+ import { z } from 'zod';
2
+ export const audioMode = z.enum(['off', 'auto', 'high', 'low']);
3
+ export const AUDIO_HEADER_BYTES = 40;
4
+ export const AUDIO_MAX_PAYLOAD = 1000;
5
+ export const audioConfiguration = z.strictObject({
6
+ commandId: z.string().regex(/^[0-9a-f]{32}$/), audioEpoch: z.number().int().min(1).max(0xffffffff),
7
+ sampleRate: z.literal(48000), channels: z.literal(2), framesPerPacket: z.literal(960),
8
+ bitrate: z.number().int().min(64000).max(256000), lookaheadFrames: z.number().int().min(0).max(960),
9
+ });
10
+ function requireAudio(value) {
11
+ if (!value)
12
+ throw new Error('invalid_audio');
13
+ }
14
+ export function decodeAudio(bytes) {
15
+ requireAudio(bytes.length >= 41 && bytes.length <= 1040 && bytes.subarray(0, 4).equals(Buffer.from('AXDA')));
16
+ requireAudio(bytes[4] === 1 && bytes[5] === 0 && bytes.readUInt16BE(6) === 40 &&
17
+ bytes.readUInt16BE(32) === 960 && bytes[34] === 2 && bytes[35] === 0 &&
18
+ bytes.readUInt16BE(38) === 0 && bytes.readUInt16BE(36) === bytes.length - 40);
19
+ const packet = { epoch: bytes.readUInt32BE(8), sequence: bytes.readUInt32BE(12),
20
+ firstSample: Number(bytes.readBigUInt64BE(16)), captureTimestampUs: Number(bytes.readBigUInt64BE(24)),
21
+ payload: bytes.subarray(40) };
22
+ validate(packet);
23
+ return packet;
24
+ }
25
+ function validate(packet) {
26
+ requireAudio(Number.isInteger(packet.epoch) && packet.epoch >= 1 && packet.epoch <= 0xffffffff &&
27
+ Number.isInteger(packet.sequence) && packet.sequence >= 1 && packet.sequence <= 0xffffffff &&
28
+ Number.isSafeInteger(packet.firstSample) && packet.firstSample === (packet.sequence - 1) * 960 &&
29
+ Number.isSafeInteger(packet.captureTimestampUs) && packet.captureTimestampUs >= 0 &&
30
+ packet.payload.length >= 1 && packet.payload.length <= AUDIO_MAX_PAYLOAD);
31
+ }
32
+ export function encodeAudio(packet) {
33
+ validate(packet);
34
+ const bytes = Buffer.alloc(AUDIO_HEADER_BYTES + packet.payload.length);
35
+ bytes.write('AXDA');
36
+ bytes[4] = 1;
37
+ bytes.writeUInt16BE(40, 6);
38
+ bytes.writeUInt32BE(packet.epoch, 8);
39
+ bytes.writeUInt32BE(packet.sequence, 12);
40
+ bytes.writeBigUInt64BE(BigInt(packet.firstSample), 16);
41
+ bytes.writeBigUInt64BE(BigInt(packet.captureTimestampUs), 24);
42
+ bytes.writeUInt16BE(960, 32);
43
+ bytes[34] = 2;
44
+ bytes.writeUInt16BE(packet.payload.length, 36);
45
+ packet.payload.copy(bytes, 40);
46
+ return bytes;
47
+ }
48
+ export class AudioRateController {
49
+ mode;
50
+ ladder = [64000, 96000, 128000, 192000, 256000];
51
+ current = 128000;
52
+ bad = 0;
53
+ healthy = 0;
54
+ lastObservation = -Infinity;
55
+ lastDown = -Infinity;
56
+ lastUp = -Infinity;
57
+ constructor(mode = 'off') {
58
+ this.mode = mode;
59
+ }
60
+ get bitrate() {
61
+ return this.mode === 'off' ? 0 : this.mode === 'high' ? 256000 : this.mode === 'low' ? 64000 : this.current;
62
+ }
63
+ select(mode) {
64
+ audioMode.parse(mode);
65
+ if (mode !== this.mode) {
66
+ this.current = mode === 'auto' ? 128000 : this.current;
67
+ this.bad = this.healthy = 0;
68
+ this.lastObservation = this.lastDown = this.lastUp = -Infinity;
69
+ }
70
+ this.mode = mode;
71
+ return this.bitrate;
72
+ }
73
+ observe(now, feedback) {
74
+ if (this.mode !== 'auto' || !Number.isFinite(now) || now - this.lastObservation < 500)
75
+ return this.bitrate;
76
+ if (![feedback.queueMs, feedback.lateRatio, feedback.lostRatio, feedback.underruns].every(Number.isFinite) ||
77
+ feedback.queueMs < 0 || feedback.lateRatio < 0 || feedback.lateRatio > 1 ||
78
+ feedback.lostRatio < 0 || feedback.lostRatio > 1 || feedback.underruns < 0)
79
+ return this.bitrate;
80
+ this.lastObservation = now;
81
+ const adverse = feedback.congested || feedback.queueMs > 40 || feedback.lateRatio > 0.02 || feedback.lostRatio > 0.03;
82
+ const recovered = !adverse && feedback.headroom && feedback.queueMs < 15 &&
83
+ feedback.lateRatio < 0.005 && feedback.lostRatio < 0.005 && feedback.underruns === 0;
84
+ this.bad = adverse ? this.bad + 1 : 0;
85
+ this.healthy = recovered ? this.healthy + 1 : 0;
86
+ const index = this.ladder.indexOf(this.current);
87
+ if (this.bad >= 2 && now - this.lastDown >= 1000 && index > 0) {
88
+ this.current = this.ladder[index - 1];
89
+ this.bad = this.healthy = 0;
90
+ this.lastDown = now;
91
+ }
92
+ else if (this.healthy >= 10 && now - Math.max(this.lastUp, this.lastDown) >= 5000 && index < this.ladder.length - 1) {
93
+ this.current = this.ladder[index + 1];
94
+ this.bad = this.healthy = 0;
95
+ this.lastUp = now;
96
+ }
97
+ return this.bitrate;
98
+ }
99
+ }
@@ -0,0 +1,219 @@
1
+ import WebSocket from 'ws';
2
+ import { automationSchema, decodeAutomationRequest, decodeAutomationResponse, encodeAutomationBinary } from './automationProtocol.js';
3
+ import { parseAutomationJson } from './automationJson.js';
4
+ import { desktopSocketUrl } from './deviceSockets.js';
5
+ import { bounded, NativeCommandError } from './helperProcess.js';
6
+ import { managedAutomationBinding } from './automationLocalState.js';
7
+ export class AutomationDeviceConnections {
8
+ server;
9
+ provider;
10
+ allowPrivateWs;
11
+ localStateDirectory;
12
+ admission;
13
+ get activeCount() { return this.active.size; }
14
+ active = new Map();
15
+ generation;
16
+ constructor(server, provider, allowPrivateWs = false, localStateDirectory, admission) {
17
+ this.server = server;
18
+ this.provider = provider;
19
+ this.allowPrivateWs = allowPrivateWs;
20
+ this.localStateDirectory = localStateDirectory;
21
+ this.admission = admission;
22
+ }
23
+ ready(generation) { this.close(); this.generation = generation; }
24
+ retired(generation) { if (generation === this.generation) {
25
+ this.close();
26
+ this.generation = undefined;
27
+ } }
28
+ close() { for (const entry of this.active.values())
29
+ entry.close(); this.active.clear(); }
30
+ async prepare(grant, binding, generation) {
31
+ if (generation !== this.generation || binding.route.deviceGeneration !== generation || this.active.size >= 8 || this.active.has(grant))
32
+ throw new Error('stale_generation');
33
+ if (!this.localStateDirectory)
34
+ throw new Error('trusted_storage_unavailable');
35
+ const localBinding = await managedAutomationBinding(this.localStateDirectory, { ...binding, registrationId: this.admission?.registrationId ?? binding.registrationId });
36
+ if (generation !== this.generation || this.active.size >= 8 || this.active.has(grant))
37
+ throw new Error('stale_generation');
38
+ const socket = new WebSocket(desktopSocketUrl(this.server, 'automation', this.allowPrivateWs), {
39
+ headers: { 'X-Axom-Automation-Grant': grant, 'X-Axom-Automation-Features': 'capture_wait_v1' }, followRedirects: false, perMessageDeflate: false, maxPayload: 49152
40
+ });
41
+ let selectedWait = false;
42
+ socket.once('upgrade', response => {
43
+ const selected = response.headers['x-axom-automation-features'];
44
+ if (selected !== undefined && selected !== 'capture_wait_v1') {
45
+ socket.terminate();
46
+ return;
47
+ }
48
+ selectedWait = selected === 'capture_wait_v1';
49
+ });
50
+ let session, retired = false, leaseBusy = false;
51
+ let mode = 'live';
52
+ let statusRequest;
53
+ const pending = new Map();
54
+ let lease;
55
+ const close = () => {
56
+ if (retired)
57
+ return;
58
+ retired = true;
59
+ clearInterval(lease);
60
+ pending.forEach(controller => controller.abort());
61
+ pending.clear();
62
+ this.active.delete(grant);
63
+ socket.terminate();
64
+ void session?.dispose().catch(() => { });
65
+ };
66
+ const current = () => !retired && this.generation === generation;
67
+ this.active.set(grant, { generation, close });
68
+ socket.on('error', close);
69
+ socket.on('close', close);
70
+ const send = async (bytes) => {
71
+ if (!current() || socket.readyState !== WebSocket.OPEN || socket.bufferedAmount > 8388608)
72
+ throw new Error('transport_failed');
73
+ await bounded(new Promise((resolve, reject) => socket.send(bytes, error => error ? reject(error) : resolve())), 5000);
74
+ };
75
+ try {
76
+ await bounded(new Promise((resolve, reject) => { socket.once('open', resolve); socket.once('error', reject); socket.once('close', () => reject(new Error('closed'))); }), 3000);
77
+ if (!this.provider?.prepareAutomation) {
78
+ await send(JSON.stringify({ automationProtocol: 1, operations: [] }));
79
+ return;
80
+ }
81
+ try {
82
+ session = await this.provider.prepareAutomation(localBinding);
83
+ }
84
+ catch {
85
+ await send(JSON.stringify({ automationProtocol: 1, operations: [] }));
86
+ return;
87
+ }
88
+ if (!current()) {
89
+ await session.dispose();
90
+ return;
91
+ }
92
+ await send(JSON.stringify({ automationProtocol: 1, operations: session.operations.filter(method => Object.hasOwn(automationSchema.requests, method) && (method !== 'capture_wait_v1' || selectedWait)) }));
93
+ const backend = session;
94
+ lease = setInterval(() => {
95
+ if (leaseBusy || !current() || mode !== 'live')
96
+ return;
97
+ leaseBusy = true;
98
+ if (this.admission && !this.admission.current(localBinding)) {
99
+ close();
100
+ return;
101
+ }
102
+ void backend.renewAuthenticatedLease(3000).catch(() => { if (mode === 'live')
103
+ close(); }).finally(() => { leaseBusy = false; });
104
+ }, 1000);
105
+ socket.on('message', (bytes, binary) => {
106
+ if (!current() || binary) {
107
+ close();
108
+ return;
109
+ }
110
+ try {
111
+ const text = bytes.toString(), raw = parseAutomationJson(text, 49152).value;
112
+ if (raw.command === 'cancel' && Object.keys(raw).length === 2) {
113
+ pending.get(raw.requestId)?.abort();
114
+ return;
115
+ }
116
+ if (raw.command === 'status_only' || raw.command === 'isolate') {
117
+ if (pending.size >= 64 || pending.has(raw.requestId)) {
118
+ close();
119
+ return;
120
+ }
121
+ mode = 'detaching';
122
+ clearInterval(lease);
123
+ const controller = new AbortController();
124
+ pending.set(raw.requestId, controller);
125
+ const operation = raw.command === 'status_only' ? backend.detachApplicationStatus?.(raw.launchRequestId, raw.canonicalArgsDigest) : backend.isolate?.();
126
+ void (operation ?? Promise.reject(new Error('unsupported'))).then(async (result) => {
127
+ if (raw.command === 'status_only') {
128
+ mode = 'status_only';
129
+ statusRequest = raw.launchRequestId;
130
+ }
131
+ if (current() && !controller.signal.aborted)
132
+ await send(JSON.stringify({ control: raw.command, id: raw.requestId, result }));
133
+ }, async (error) => {
134
+ if (current() && !controller.signal.aborted)
135
+ await send(JSON.stringify({ control: 'prepare_failed', id: raw.requestId, code: error instanceof Error ? error.message : 'service_unavailable', detail: null }));
136
+ }).catch(close).finally(() => pending.delete(raw.requestId));
137
+ return;
138
+ }
139
+ if (raw.command === 'prepare' && Object.keys(raw).length === 3) {
140
+ if (this.admission && !this.admission.current(localBinding))
141
+ throw new Error('control_lost');
142
+ if (mode !== 'live') {
143
+ void send(JSON.stringify({ control: 'prepare_failed', id: raw.requestId, code: 'control_lost', detail: null })).catch(close);
144
+ return;
145
+ }
146
+ if (pending.size >= 64 || pending.has(raw.requestId)) {
147
+ close();
148
+ return;
149
+ }
150
+ const controller = new AbortController();
151
+ pending.set(raw.requestId, controller);
152
+ void backend.prepareAttachment(raw.preparation, { signal: controller.signal }).then(attachment => { if (current() && !controller.signal.aborted)
153
+ return send(JSON.stringify({ control: 'prepared', id: raw.requestId, attachment })); }, error => { if (current() && !controller.signal.aborted)
154
+ return send(JSON.stringify({ control: 'prepare_failed', id: raw.requestId, code: error instanceof Error ? error.message : 'service_unavailable', detail: error instanceof NativeCommandError ? error.detail : null })); }).catch(close).finally(() => pending.delete(raw.requestId));
155
+ return;
156
+ }
157
+ const request = decodeAutomationRequest(text);
158
+ if (this.admission && !this.admission.current(localBinding) && request.method !== 'close')
159
+ throw new Error('control_lost');
160
+ if (mode !== 'live' && (mode !== 'status_only' || request.method !== 'application_status' || request.payload.requestId !== statusRequest)) {
161
+ void send(JSON.stringify({ ...request, payload: { type: 'failure', code: 'control_lost', message: 'Input authority was retired', detail: null } })).catch(close);
162
+ return;
163
+ }
164
+ if (pending.size >= 64 || pending.has(request.requestId)) {
165
+ close();
166
+ return;
167
+ }
168
+ if (request.method === 'capture_wait_v1' && (!selectedWait || !backend.operations.includes(request.method))) {
169
+ void send(JSON.stringify({ ...request, payload: { type: 'failure', code: 'unsupported', message: 'Conditional observation was not negotiated', detail: null } })).catch(close);
170
+ return;
171
+ }
172
+ const controller = new AbortController();
173
+ pending.set(request.requestId, controller);
174
+ void (async () => {
175
+ const reply = await backend.execute(request, { signal: controller.signal });
176
+ if (!current() || controller.signal.aborted)
177
+ return;
178
+ let response = decodeAutomationResponse(JSON.stringify(reply.response), request);
179
+ let binary;
180
+ if (reply.image) {
181
+ const data = response.payload.data, transfer = request.method === 'capture' ? data : request.method === 'capture_wait_v1' ? data.image : data.postImage;
182
+ binary = encodeAutomationBinary(transfer.header, reply.image);
183
+ const captureAgeMs = Date.now() - transfer.metadata.capturedAtEpochMs;
184
+ if (!Number.isSafeInteger(captureAgeMs) || captureAgeMs < 0 || captureAgeMs > 30000) {
185
+ if (request.method !== 'act' && request.method !== 'status')
186
+ throw new Error('stale_observation');
187
+ response = { ...response, payload: { type: 'success', data: { ...data, postImage: null, captureError: 'capture_failed' } } };
188
+ binary = undefined;
189
+ }
190
+ else
191
+ response = { ...response, captureAgeMs };
192
+ }
193
+ await send(JSON.stringify(response));
194
+ if (binary)
195
+ await send(binary);
196
+ })().catch(async (error) => {
197
+ if (!current() || controller.signal.aborted)
198
+ return;
199
+ const code = error instanceof Error && error.message === 'stale_observation' ? 'stale_observation'
200
+ : error instanceof Error && error.message === 'unsupported' ? 'unsupported' : 'service_unavailable';
201
+ try {
202
+ await send(JSON.stringify({ ...request, payload: { type: 'failure', code, message: error instanceof Error ? error.message : code, detail: null } }));
203
+ }
204
+ catch {
205
+ close();
206
+ }
207
+ }).finally(() => pending.delete(request.requestId));
208
+ }
209
+ catch {
210
+ close();
211
+ }
212
+ });
213
+ }
214
+ catch (error) {
215
+ close();
216
+ throw error;
217
+ }
218
+ }
219
+ }