@platless/axom-cli 0.5.9 → 0.6.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/README.md +25 -6
  2. package/dist/axom-patch/cli/README.md +129 -12
  3. package/dist/axom-patch/cli/account-limits/generate.py +15 -12
  4. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
  5. package/dist/axom-patch/cli/claude-account-limits.json +9 -9
  6. package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
  7. package/dist/axom-patch/cli/codex-account-limits.json +11 -11
  8. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  9. package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
  10. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  11. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  12. package/dist/axom-patch/cli/nightly-task.md +55 -13
  13. package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
  14. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  15. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  16. package/dist/axom-patch/cli/probes/zcode-env-account.mjs +81 -0
  17. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
  18. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1687 -0
  19. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +277 -0
  20. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
  21. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
  22. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  23. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  24. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  25. package/dist/axom-patch/cli/runtime.json +19 -19
  26. package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
  27. package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
  28. package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
  29. package/dist/axom-patch/cli/zcode-env-account.json +58 -0
  30. package/dist/axom-patch/cli/zcode-models.json +9 -10
  31. package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -11
  32. package/dist/cli.js +11 -45
  33. package/dist/commands/auth.js +4 -1
  34. package/dist/commands/knowledge.js +47 -0
  35. package/dist/commands/update.js +126 -3
  36. package/dist/commands/workstation.js +101 -164
  37. package/dist/commands/workstationRegistrations.js +116 -0
  38. package/dist/credentialBroker.js +13 -2
  39. package/dist/credentials.js +16 -0
  40. package/dist/install/claude/commands.js +15 -15
  41. package/dist/keepers/terminal/keeper.mjs +1089 -673
  42. package/dist/keepers/terminal/manifest.json +4 -4
  43. package/dist/mcp.js +1 -1
  44. package/dist/native/darwin-x64/axom-quic +0 -0
  45. package/dist/workstation/acp/acpAgents.js +113 -102
  46. package/dist/workstation/acp/acpClient.js +26 -0
  47. package/dist/workstation/acp/builds/install/npm.js +99 -10
  48. package/dist/workstation/acp/initializeResponse.js +6 -1
  49. package/dist/workstation/acp/managedPatches.js +189 -25
  50. package/dist/workstation/acp/managedRuntime.js +314 -0
  51. package/dist/workstation/acp/outbox.js +12 -5
  52. package/dist/workstation/acp/patchAssets.js +40 -0
  53. package/dist/workstation/acp/permission.js +2 -2
  54. package/dist/workstation/acp/processor/checkpoint.js +26 -34
  55. package/dist/workstation/acp/processor/control.js +7 -80
  56. package/dist/workstation/acp/processor/jsonrpc.js +8 -0
  57. package/dist/workstation/acp/processor/runtime.js +5 -81
  58. package/dist/workstation/acp/processor/session.js +54 -82
  59. package/dist/workstation/acp/processor/state.js +7 -13
  60. package/dist/workstation/acp/processor/step.js +3 -20
  61. package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
  62. package/dist/workstation/acp/processor/transport/local.js +19 -3
  63. package/dist/workstation/acp/processor/updates.js +156 -105
  64. package/dist/workstation/acp/processorSession.js +20 -173
  65. package/dist/workstation/acp/registry/catalog.js +65 -39
  66. package/dist/workstation/acp/registry/launch.js +16 -1
  67. package/dist/workstation/acp/switching.js +0 -3
  68. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  69. package/dist/workstation/acp/test/keeperWorld.js +32 -3
  70. package/dist/workstation/acp/test/raw/sources.js +16 -130
  71. package/dist/workstation/acp/test/realAdapter.js +1 -47
  72. package/dist/workstation/acp/test/scriptAgent.js +11 -3
  73. package/dist/workstation/agentFlow.js +1 -1
  74. package/dist/workstation/agentSessions.js +114 -166
  75. package/dist/workstation/channels/agent.js +9 -1
  76. package/dist/workstation/channels/computer.js +27 -4
  77. package/dist/workstation/channels/management.js +130 -15
  78. package/dist/workstation/channels/url.js +1 -1
  79. package/dist/workstation/config.js +33 -9
  80. package/dist/workstation/connection/encryptedChannel.js +4 -1
  81. package/dist/workstation/control/bindingStore.js +204 -0
  82. package/dist/workstation/control/commands.js +125 -0
  83. package/dist/workstation/control/daemonIdentity.js +143 -0
  84. package/dist/workstation/control/deviceCommands.js +191 -0
  85. package/dist/workstation/control/deviceProtocol.js +68 -0
  86. package/dist/workstation/control/enrollment.js +106 -0
  87. package/dist/workstation/control/inspect.js +71 -0
  88. package/dist/workstation/control/locks.js +110 -0
  89. package/dist/workstation/control/protocol.js +40 -0
  90. package/dist/workstation/control/restartGuard.js +46 -0
  91. package/dist/workstation/control/rootFormat.js +16 -0
  92. package/dist/workstation/control/runtimeClient.js +152 -0
  93. package/dist/workstation/control/runtimeHost.js +235 -0
  94. package/dist/workstation/control/serverIdentity.js +35 -0
  95. package/dist/workstation/control/updateTransition.js +91 -0
  96. package/dist/workstation/daemon.js +266 -264
  97. package/dist/workstation/daemonctl.js +226 -25
  98. package/dist/workstation/desktopControl/audio.js +99 -0
  99. package/dist/workstation/desktopControl/automationDevice.js +219 -0
  100. package/dist/workstation/desktopControl/automationJson.js +74 -0
  101. package/dist/workstation/desktopControl/automationLocalState.js +23 -0
  102. package/dist/workstation/desktopControl/automationProtocol.js +253 -0
  103. package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
  104. package/dist/workstation/desktopControl/automationSession.js +81 -0
  105. package/dist/workstation/desktopControl/automationTransport.js +138 -0
  106. package/dist/workstation/desktopControl/capabilities.js +93 -0
  107. package/dist/workstation/desktopControl/captureModes.js +9 -0
  108. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  109. package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
  110. package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
  111. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  112. package/dist/workstation/desktopControl/helperProcess.js +602 -0
  113. package/dist/workstation/desktopControl/hostGrant.js +65 -0
  114. package/dist/workstation/desktopControl/lobby.js +131 -0
  115. package/dist/workstation/desktopControl/macProvider.js +156 -0
  116. package/dist/workstation/desktopControl/media.js +331 -0
  117. package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
  118. package/dist/workstation/desktopControl/nativeProvider.js +1044 -0
  119. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  120. package/dist/workstation/desktopControl/overview.js +58 -0
  121. package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
  122. package/dist/workstation/desktopControl/protocol.js +311 -0
  123. package/dist/workstation/desktopControl/provider.js +1 -0
  124. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  125. package/dist/workstation/desktopControl/rateController.js +226 -0
  126. package/dist/workstation/desktopControl/supervisor.js +1774 -0
  127. package/dist/workstation/desktopControl/transport.js +27 -0
  128. package/dist/workstation/desktopControl/wallpaper.js +98 -0
  129. package/dist/workstation/exec/policy.js +5 -0
  130. package/dist/workstation/files/client.js +10 -1
  131. package/dist/workstation/files/credentials.js +142 -0
  132. package/dist/workstation/files/host.js +9 -2
  133. package/dist/workstation/files/text.js +42 -8
  134. package/dist/workstation/journal.js +5 -2
  135. package/dist/workstation/keepers/acp/client.js +38 -2
  136. package/dist/workstation/keepers/kit/buildVersion.js +35 -11
  137. package/dist/workstation/keepers/kit/bundle.js +21 -17
  138. package/dist/workstation/keepers/kit/client.js +2 -2
  139. package/dist/workstation/keepers/kit/launcher.js +123 -7
  140. package/dist/workstation/keepers/kit/paths.js +2 -1
  141. package/dist/workstation/keepers/legacy.js +16 -2
  142. package/dist/workstation/keepers/policy.js +5 -34
  143. package/dist/workstation/keepers/terminal/client.js +115 -23
  144. package/dist/workstation/keepers/terminal/exec.js +6 -3
  145. package/dist/workstation/keepers/terminal/frames.js +73 -0
  146. package/dist/workstation/keepers/terminal/generations.js +5 -8
  147. package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
  148. package/dist/workstation/keepers/terminal/input.js +146 -0
  149. package/dist/workstation/keepers/terminal/keeper.js +22 -8
  150. package/dist/workstation/keepers/terminal/main.js +1 -1
  151. package/dist/workstation/keepers/terminal/metadata.js +138 -0
  152. package/dist/workstation/keepers/terminal/protocol.js +4 -28
  153. package/dist/workstation/keepers/terminal/pty.js +1 -330
  154. package/dist/workstation/keepers/terminal/screen.js +246 -0
  155. package/dist/workstation/keepers/terminal/sessions.js +526 -0
  156. package/dist/workstation/managedInstall.js +57 -0
  157. package/dist/workstation/oauth/deviceAuthorization.js +31 -17
  158. package/dist/workstation/protocol/agent.js +2 -15
  159. package/dist/workstation/protocol/management.js +113 -5
  160. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  161. package/dist/workstation/rd/artifact.js +120 -0
  162. package/dist/workstation/rd/control.js +117 -0
  163. package/dist/workstation/rd/display.js +29 -0
  164. package/dist/workstation/rd/host.js +213 -0
  165. package/dist/workstation/rd/migrationReadiness.js +46 -0
  166. package/dist/workstation/rd/provider.js +256 -0
  167. package/dist/workstation/rd/sessions.js +391 -0
  168. package/dist/workstation/rd/state.js +57 -0
  169. package/dist/workstation/rd/update.js +168 -0
  170. package/dist/workstation/rd/upkeep.js +80 -0
  171. package/dist/workstation/register.js +27 -37
  172. package/dist/workstation/registrations/enrollment.js +250 -0
  173. package/dist/workstation/registrations/enrollmentPending.js +361 -0
  174. package/dist/workstation/registrations/migration.js +356 -0
  175. package/dist/workstation/registrations/model.js +34 -0
  176. package/dist/workstation/registrations/removal.js +222 -0
  177. package/dist/workstation/registrations/store.js +737 -0
  178. package/dist/workstation/runtime/desktopBinding.js +20 -0
  179. package/dist/workstation/runtime/deviceDesktop.js +193 -0
  180. package/dist/workstation/runtime/deviceRuntime.js +568 -0
  181. package/dist/workstation/runtime/deviceServices.js +117 -0
  182. package/dist/workstation/runtime/events.js +20 -0
  183. package/dist/workstation/runtime/ownership.js +100 -0
  184. package/dist/workstation/runtime/registrationRuntime.js +575 -0
  185. package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
  186. package/dist/workstation/runtime/removalActivity.js +184 -0
  187. package/dist/workstation/service/installer.js +197 -14
  188. package/dist/workstation/service/launchd.js +2 -0
  189. package/dist/workstation/service/systemd.js +1 -0
  190. package/dist/workstation/storageMetrics.js +118 -0
  191. package/dist/workstation/systemMetrics.js +47 -92
  192. package/dist/workstation/toolchain.js +94 -0
  193. package/dist/workstation/tools/browser.js +28 -4
  194. package/dist/workstation/tools/client.js +8 -27
  195. package/dist/workstation/tools/info.js +6 -13
  196. package/dist/workstation/tools/terminalRouter.js +17 -172
  197. package/dist/workstation/tools/validation.js +55 -1
  198. package/dist/workstation/update/archiveEvidence.js +206 -0
  199. package/dist/workstation/update/artifact.js +2 -1
  200. package/dist/workstation/update/coordinator.js +34 -13
  201. package/dist/workstation/update/deviceCompatibility.js +119 -0
  202. package/dist/workstation/update/deviceCoordinator.js +369 -0
  203. package/dist/workstation/update/deviceHealth.js +104 -0
  204. package/dist/workstation/update/deviceHealthProtocol.js +26 -0
  205. package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
  206. package/dist/workstation/update/deviceRollback.js +78 -0
  207. package/dist/workstation/update/globalInstall.js +186 -0
  208. package/dist/workstation/update/legacyBridge.js +414 -0
  209. package/dist/workstation/update/legacyOwner.js +39 -0
  210. package/dist/workstation/update/npmInstaller.js +51 -25
  211. package/dist/workstation/update/privateDurability.js +61 -0
  212. package/dist/workstation/update/rdRollback.js +145 -0
  213. package/dist/workstation/update/recoveryPlan.js +124 -0
  214. package/dist/workstation/update/runUpdate.js +10 -3
  215. package/dist/workstation/update/taskRegistry.js +161 -3
  216. package/dist/workstation/update/watchdog.js +29806 -333
  217. package/dist/workstation/update/watchdogProcess.js +72 -0
  218. package/package.json +11 -7
  219. package/skills/axom/SKILL.md +11 -11
  220. package/skills/axom-definition/SKILL.md +3 -3
  221. package/skills/axom-platform/SKILL.md +3 -3
  222. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  223. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
  224. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  225. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  226. package/dist/commands/guide.js +0 -38
  227. package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
  228. package/dist/runtime-deps/qoder-sdk/package.json +0 -8
  229. package/dist/workstation/acp/delegationMcp.js +0 -24
  230. package/dist/workstation/acp/processor/sideChannel.js +0 -80
  231. package/dist/workstation/acp/qoderStartup.js +0 -45
  232. package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
  233. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
  234. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
  235. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
  236. package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
  237. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  238. package/dist/workstation/qoderAdapter/contract.js +0 -78
  239. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
  240. package/dist/workstation/qoderAdapter/history.js +0 -85
  241. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  242. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  243. package/dist/workstation/qoderAdapter/main.js +0 -95
  244. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  245. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  246. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  247. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  248. package/dist/workstation/qoderAdapter/profile.js +0 -93
  249. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
  250. package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
  251. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
  252. package/dist/workstation/qoderAdapter/session.js +0 -319
  253. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  254. package/dist/workstation/qoderAdapter/subwork.js +0 -85
  255. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  256. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  257. package/dist/workstation/qoderAdapter/usage.js +0 -37
  258. /package/dist/workstation/{qoderAdapter/types.js → desktopControl/automationPreparation.js} +0 -0
@@ -0,0 +1,1774 @@
1
+ import { DesktopMediaAdmissionCancelled } from './quicMedia.js';
2
+ import { DesktopOverviewBudget, overviewPacket } from './overview.js';
3
+ import { randomBytes } from 'node:crypto';
4
+ import { decodeLobbyRequest, planLobby } from './lobby.js';
5
+ import { setTimeout as dependencyDelay } from 'node:timers/promises';
6
+ import { bounded, NativeCommandError } from './helperProcess.js';
7
+ import { decodeMedia, mediaRequire, DesktopMediaFlowWindow, desktopMediaLane, DesktopDeliveryDelay, decodeConfig } from './media.js';
8
+ import { DesktopRateController } from './rateController.js';
9
+ import { AudioRateController, decodeAudio } from './audio.js';
10
+ import { canonicalDesktopJson, encodeDesktopControl, decodeDesktopManagementFrame, DesktopProtocolError } from './protocol.js';
11
+ const absoluteMotion = (message) => message.type === 'input' && message.payload.action === 'pointer';
12
+ const diagnosticCodes = new Set(['invalid_request', 'unavailable', 'conflict', 'busy', 'rate_limited',
13
+ 'authorization_lost', 'encoder_failed', 'resize_failed', 'transport_failed']);
14
+ /** One authority owner and bounded management work queue; receive/heartbeat never await native I/O. */
15
+ export class DesktopSessionSupervisor {
16
+ options;
17
+ get activeCount() { return Number(!!this.active && !this.active.retired) + this.candidates.size; }
18
+ generation;
19
+ active;
20
+ candidates = new Map();
21
+ tail = Promise.resolve();
22
+ pending = 0;
23
+ closed = false;
24
+ failedCleanup = false;
25
+ commands = new Map();
26
+ diagnosticId = 0;
27
+ lobbyJobs = 0;
28
+ lobbyContexts = new Map();
29
+ backgroundTail = Promise.resolve();
30
+ backgroundJobs = 0;
31
+ cleanupJobs = 0;
32
+ retainedSessions = new Map();
33
+ maintenance;
34
+ constructor(options) {
35
+ this.options = options;
36
+ }
37
+ async whenIdle(operation) {
38
+ if (this.closed || !this.generation || this.failedCleanup || this.maintenance || this.active || this.candidates.size ||
39
+ this.pending || this.cleanupJobs || this.lobbyJobs || this.backgroundJobs ||
40
+ [...this.lobbyContexts.values()].some(context => context.expires > Date.now()))
41
+ return false;
42
+ const maintenance = Promise.resolve().then(operation);
43
+ this.maintenance = maintenance;
44
+ try {
45
+ await maintenance;
46
+ return true;
47
+ }
48
+ finally {
49
+ if (this.maintenance === maintenance)
50
+ this.maintenance = undefined;
51
+ }
52
+ }
53
+ managementReady(generation) {
54
+ if (this.generation === generation)
55
+ return;
56
+ const previous = this.active;
57
+ this.generation = generation;
58
+ this.commands.clear();
59
+ this.clearLobbies();
60
+ if (previous) {
61
+ this.retire(previous);
62
+ void this.enqueue(() => this.cleanup(previous)).catch(() => { });
63
+ }
64
+ for (const candidate of [...this.candidates.values()]) {
65
+ this.retire(candidate);
66
+ void this.cleanup(candidate).catch(() => { });
67
+ }
68
+ }
69
+ managementClosed(generation) {
70
+ if (this.generation !== generation)
71
+ return;
72
+ this.generation = undefined;
73
+ this.clearLobbies();
74
+ const previous = this.active;
75
+ if (previous) {
76
+ this.retire(previous);
77
+ void this.enqueue(() => this.cleanup(previous)).catch(() => { });
78
+ }
79
+ for (const candidate of [...this.candidates.values()]) {
80
+ this.retire(candidate);
81
+ void this.cleanup(candidate).catch(() => { });
82
+ }
83
+ }
84
+ dispatch(frame, generation) {
85
+ if (this.maintenance)
86
+ return this.maintenance.catch(() => { }).then(() => this.dispatch(frame, generation));
87
+ if (generation !== this.generation)
88
+ return Promise.resolve();
89
+ if (frame.t === 'desktop_background')
90
+ return this.background(frame);
91
+ if (frame.t === 'desktop_lobby')
92
+ return this.planLobby(frame);
93
+ if (frame.t === 'desktop_prepare')
94
+ return this.prepare(frame);
95
+ if (frame.requireIsolation)
96
+ return this.isolate(frame, generation);
97
+ return this.cancel(frame.sessionId, { commandId: frame.commandId, deviceGeneration: generation, controlGeneration: frame.controlGeneration });
98
+ }
99
+ async isolate(frame, generation) {
100
+ const key = `${generation}:${frame.sessionId}:${frame.controlGeneration}`;
101
+ const retained = this.retainedSessions.get(key);
102
+ let isolated = false;
103
+ if (retained) {
104
+ this.retire(retained);
105
+ try {
106
+ await this.cleanup(retained);
107
+ isolated = true;
108
+ }
109
+ catch { }
110
+ }
111
+ if (this.generation === generation && !this.closed)
112
+ this.options.report(generation, {
113
+ t: 'desktop_isolation_result', commandId: frame.commandId, sessionId: frame.sessionId,
114
+ deviceGeneration: generation, controlGeneration: frame.controlGeneration, isolated,
115
+ });
116
+ }
117
+ background(frame) {
118
+ if (this.closed || frame.deviceGeneration !== this.generation || this.backgroundJobs >= 16)
119
+ return Promise.resolve();
120
+ this.backgroundJobs++;
121
+ const current = () => !this.closed && frame.deviceGeneration === this.generation &&
122
+ this.lobbyContexts.get(frame.lobbyId)?.commandId === frame.planId &&
123
+ (this.lobbyContexts.get(frame.lobbyId)?.expires ?? 0) > Date.now();
124
+ const next = this.backgroundTail.then(async () => {
125
+ if (!current() || !this.options.sockets.uploadWallpaper)
126
+ return;
127
+ try {
128
+ const wallpaper = this.options.provider.wallpaper ? await bounded(this.options.provider.wallpaper(frame.targetId, { width: frame.pixelWidth, height: frame.pixelHeight }), 8000) : null;
129
+ if (current())
130
+ await this.options.sockets.uploadWallpaper(frame.grant, wallpaper, wallpaper?.asset.version === frame.cachedVersion);
131
+ }
132
+ catch {
133
+ if (current())
134
+ await this.options.sockets.uploadWallpaper(frame.grant, null).catch(() => { });
135
+ }
136
+ });
137
+ this.backgroundTail = next.catch(() => { }).finally(() => { this.backgroundJobs--; });
138
+ return next;
139
+ }
140
+ async planLobby(frame) {
141
+ if (this.closed || frame.deviceGeneration !== this.generation || this.lobbyJobs >= 8)
142
+ return;
143
+ for (const [key, context] of this.lobbyContexts)
144
+ if (context.expires <= Date.now()) {
145
+ context.abort.abort();
146
+ this.lobbyContexts.delete(key);
147
+ }
148
+ if (!this.lobbyContexts.has(frame.lobbyId) && this.lobbyContexts.size >= 64)
149
+ return;
150
+ this.lobbyContexts.get(frame.lobbyId)?.abort.abort();
151
+ const context = { commandId: frame.commandId, abort: new AbortController(), expires: Date.now() + 30_000 };
152
+ this.lobbyContexts.set(frame.lobbyId, context);
153
+ const current = () => !this.closed && this.generation === frame.deviceGeneration &&
154
+ this.lobbyContexts.get(frame.lobbyId) === context && !context.abort.signal.aborted;
155
+ this.lobbyJobs++;
156
+ try {
157
+ const request = decodeLobbyRequest(JSON.stringify(frame.request));
158
+ const capabilities = await bounded(this.options.provider.probe(), 4000);
159
+ if (!current())
160
+ return;
161
+ mediaRequire(capabilities != null);
162
+ const plan = planLobby(request, capabilities, frame.lobbyId, frame.commandId, this.options.provider.automaticDpi ?? true);
163
+ const probeAbort = new AbortController();
164
+ const abortProbe = () => probeAbort.abort();
165
+ context.abort.signal.addEventListener('abort', abortProbe, { once: true });
166
+ const binding = { commandId: frame.commandId, sessionId: frame.lobbyId, displayId: plan.targetId,
167
+ deviceGeneration: frame.deviceGeneration, controlGeneration: request.revision, controlGrant: '' };
168
+ const verify = async (grant, quic) => {
169
+ let socket;
170
+ try {
171
+ socket = await bounded(this.options.sockets.media({ ...binding, mediaTransport: quic ? 'quic' : 'wss' }, grant, probeAbort.signal, () => current() && !probeAbort.signal.aborted, quic), 2000);
172
+ }
173
+ catch { }
174
+ finally {
175
+ socket?.close();
176
+ }
177
+ };
178
+ const probing = [frame.wssProbeGrant ? verify(frame.wssProbeGrant) : Promise.resolve(),
179
+ frame.quicProbeGrant && frame.quic ? verify(frame.quicProbeGrant, frame.quic) : Promise.resolve()];
180
+ void Promise.all(probing).finally(() => {
181
+ probeAbort.abort();
182
+ context.abort.signal.removeEventListener('abort', abortProbe);
183
+ });
184
+ if (current())
185
+ this.options.report(frame.deviceGeneration, { t: 'desktop_lobby_result', commandId: frame.commandId, lobbyId: frame.lobbyId,
186
+ deviceGeneration: frame.deviceGeneration, plan, code: null });
187
+ }
188
+ catch {
189
+ if (current())
190
+ this.options.report(frame.deviceGeneration, { t: 'desktop_lobby_result', commandId: frame.commandId, lobbyId: frame.lobbyId,
191
+ deviceGeneration: frame.deviceGeneration, plan: null, code: 'unavailable' });
192
+ }
193
+ finally {
194
+ this.lobbyJobs--;
195
+ }
196
+ }
197
+ clearLobbies() {
198
+ for (const context of this.lobbyContexts.values())
199
+ context.abort.abort();
200
+ this.lobbyContexts.clear();
201
+ }
202
+ enqueue(run) {
203
+ mediaRequire(this.pending < 16);
204
+ this.pending++;
205
+ const next = this.tail.then(run);
206
+ this.tail = next.catch(() => { }).finally(() => { this.pending--; });
207
+ return next;
208
+ }
209
+ preparationCurrent(binding) {
210
+ if (this.options.preparationCurrent)
211
+ return this.options.preparationCurrent(binding);
212
+ let begun = false;
213
+ return (beginEffect = false) => {
214
+ if (this.closed || binding.deviceGeneration !== this.generation || this.options.controllerCurrent?.(binding) === false)
215
+ throw new NativeCommandError('control_lost');
216
+ if (!begun && this.options.newWorkAllowed?.() === false)
217
+ return false;
218
+ if (beginEffect)
219
+ begun = true;
220
+ return true;
221
+ };
222
+ }
223
+ prepare(command) {
224
+ if (this.maintenance)
225
+ return this.maintenance.catch(() => { }).then(() => this.prepare(command));
226
+ decodeDesktopManagementFrame(JSON.stringify({ ...command, t: 'desktop_prepare' }), 'server');
227
+ if (this.closed || command.deviceGeneration !== this.generation)
228
+ return Promise.resolve();
229
+ if (this.options.newWorkAllowed?.() === false) {
230
+ this.options.report(command.deviceGeneration, { t: 'desktop_prepare_result', commandId: command.commandId, sessionId: command.sessionId, state: 'failed', code: 'busy' });
231
+ return Promise.resolve();
232
+ }
233
+ mediaRequire(this.pending < 16);
234
+ const key = `${command.deviceGeneration}:${command.commandId}`, serialized = canonicalDesktopJson(command);
235
+ const previousCommand = this.commands.get(key);
236
+ if (previousCommand) {
237
+ mediaRequire(previousCommand === serialized);
238
+ return Promise.resolve();
239
+ }
240
+ mediaRequire(this.commands.size < 1024);
241
+ this.commands.set(key, serialized);
242
+ mediaRequire(!command.candidate || this.candidates.size < 8);
243
+ const previous = command.candidate ? undefined : this.active;
244
+ const openingCurrent = previous ? this.preparationCurrent(command) : () => this.options.newWorkAllowed?.() !== false;
245
+ if (previous) {
246
+ if (!openingCurrent(true))
247
+ return Promise.resolve();
248
+ this.retire(previous);
249
+ }
250
+ const s = { diagnosticId: ++this.diagnosticId, diagnosticCount: 0, rateDiagnosticCount: 0, lastRateDiagnosticAt: -Infinity,
251
+ performanceDiagnosticCount: 0, lastPerformanceDiagnosticAt: -Infinity, diagnosticKey: randomBytes(16).toString('hex'), motionEncodeMs: 0,
252
+ binding: { ...command }, abort: new AbortController(), retired: false, viewerReady: false, starting: false,
253
+ startedEpoch: 0, decoderReady: false, presented: false, inputQueue: [], inputRunning: false, activeTasks: 0, commands: new Map(),
254
+ controller: new DesktopRateController({ fpsCeiling: 30, maxFps: 30 }), rateBusy: false, capturePaused: false, lastFeedbackBytes: 0,
255
+ activeBytes: 0, queuedAt: 0, lastPacketAt: performance.now(), receivedFrame: 0, firstCompleteFrame: 0, flow: new DesktopMediaFlowWindow(), delivery: new DesktopDeliveryDelay(), packetLoop: false, mediaWake: new Set(), nativeCreditDirty: false, nativeDisposed: { motion: 0, cursor: 0, exact: 0 }, configAdmittedEpoch: 0, recovery: [],
256
+ overviewBudget: new DesktopOverviewBudget(), overviewSequence: 0, overviewInitialPolls: 0, challenges: new Map(), rtts: [], rtt: 0, nextPing: performance.now() + 10000 };
257
+ mediaRequire(this.retainedSessions.size < 1024);
258
+ this.retainedSessions.set(`${command.deviceGeneration}:${command.sessionId}:${command.controlGeneration}`, s);
259
+ if (command.candidate)
260
+ this.candidates.set(s.binding.sessionId, s);
261
+ else
262
+ this.active = s;
263
+ const prepare = async () => {
264
+ if (previous)
265
+ await this.cleanup(previous);
266
+ if (!this.current(s))
267
+ return;
268
+ if (!openingCurrent()) {
269
+ this.options.report(command.deviceGeneration, { t: 'desktop_prepare_result', commandId: command.commandId, sessionId: command.sessionId, state: 'failed', code: 'busy' });
270
+ this.retire(s);
271
+ await this.cleanup(s);
272
+ return;
273
+ }
274
+ mediaRequire(!this.failedCleanup);
275
+ try {
276
+ // Discovery is side-effect free and independent of the authenticated socket attach.
277
+ const discovered = bounded(this.options.provider.probe(), 4000);
278
+ // Attach can fail first. Observe discovery's rejection until it is awaited below.
279
+ void discovered.catch(() => { });
280
+ const control = await bounded(this.options.sockets.control(s.binding, s.abort.signal, () => this.current(s)), 5000);
281
+ s.control = control;
282
+ if (!this.current(s)) {
283
+ control.close();
284
+ return;
285
+ }
286
+ const caps = await discovered;
287
+ if (!this.current(s))
288
+ return;
289
+ if (caps && !caps.displays.some(display => display.id === command.displayId))
290
+ this.options.refreshCapabilities?.(command.deviceGeneration);
291
+ mediaRequire(caps && caps.health === 'ready' && caps.displays.some(d => d.id === command.displayId));
292
+ this.options.report(command.deviceGeneration, { t: 'desktop_prepare_result', commandId: command.commandId, sessionId: command.sessionId, state: 'bound', code: null });
293
+ await this.send(s, { v: 1, type: 'offer', sessionId: command.sessionId, payload: caps });
294
+ void this.controlLoop(s);
295
+ s.timer = setInterval(() => {
296
+ const backend = s.backend, configured = s.configured;
297
+ void this.rateTick(s).catch(error => {
298
+ if (this.current(s) && s.backend === backend && s.configured === configured) {
299
+ this.diagnostic(s, 'rate_tick', error);
300
+ return this.recover(s);
301
+ }
302
+ });
303
+ }, 100);
304
+ s.timer.unref();
305
+ }
306
+ catch (error) {
307
+ if (this.current(s)) {
308
+ this.diagnostic(s, 'prepare', error);
309
+ this.options.report(command.deviceGeneration, { t: 'desktop_prepare_result', commandId: command.commandId, sessionId: command.sessionId, state: 'failed', code: 'unavailable' });
310
+ await this.fail(s);
311
+ }
312
+ }
313
+ };
314
+ return command.candidate ? prepare() : this.enqueue(prepare);
315
+ }
316
+ cancel(sessionId, identity) {
317
+ const s = this.active?.binding.sessionId === sessionId ? this.active : this.candidates.get(sessionId);
318
+ if (!s || s.binding.sessionId !== sessionId || identity && (identity.deviceGeneration !== s.binding.deviceGeneration || identity.controlGeneration !== s.binding.controlGeneration))
319
+ return Promise.resolve();
320
+ if (identity) {
321
+ const key = `${identity.deviceGeneration}:${identity.commandId}`, value = canonicalDesktopJson({ sessionId, ...identity });
322
+ const old = this.commands.get(key);
323
+ if (old) {
324
+ mediaRequire(old === value);
325
+ return Promise.resolve();
326
+ }
327
+ mediaRequire(this.commands.size < 1024);
328
+ this.commands.set(key, value);
329
+ }
330
+ this.retire(s);
331
+ return this.enqueue(() => this.cleanup(s));
332
+ }
333
+ current(s) {
334
+ return !this.closed && !s.retired &&
335
+ (this.active === s || this.candidates.get(s.binding.sessionId) === s) && this.generation === s.binding.deviceGeneration &&
336
+ (s.binding.candidate || this.options.controllerCurrent?.(s.binding) !== false);
337
+ }
338
+ diagnostic(s, origin, error, packet, command) {
339
+ // At most 16 fixed-shape records per session, before recovery resets its state.
340
+ // Never log error text/stacks, bindings, grants, input coordinates/text or media.
341
+ try {
342
+ if (s.diagnosticCount >= 16)
343
+ return;
344
+ const counters = s.backend?.nativeTelemetry;
345
+ const code = error instanceof NativeCommandError && diagnosticCodes.has(error.code) ? error.code :
346
+ error instanceof DesktopProtocolError ? 'protocol_invalid' : error === undefined ? 'none' : 'unexpected_error';
347
+ console.error(JSON.stringify({ event: 'desktop_supervisor_diagnostic', session: s.diagnosticId,
348
+ ordinal: ++s.diagnosticCount, origin, code, packet, command,
349
+ epoch: s.configured?.mediaEpoch ?? 0, startedEpoch: s.startedEpoch, receivedFrame: s.receivedFrame,
350
+ decoderReady: s.decoderReady, presented: s.presented, recovering: s.recovering === true,
351
+ activeTasks: s.activeTasks, queuedInputs: s.inputQueue.length, oldestInputMs: s.inputQueue.length ? Math.max(0, Math.round(performance.now() - s.inputQueue[0].at)) : 0, mediaAttached: !!s.media, inFlightBytes: s.flow.inFlightBytes,
352
+ queuedMs: s.queuedAt ? Math.max(0, Math.round(performance.now() - s.queuedAt)) : 0,
353
+ native: counters ? { epoch: counters.epoch, captured: counters.captured, encoded: counters.encoded,
354
+ sentPayloadBytes: counters.sentPayloadBytes, pendingPayloadBytes: counters.pendingPayloadBytes,
355
+ inputAvailable: counters.inputAvailable, pauseCapture: counters.pauseCapture } : undefined }));
356
+ }
357
+ catch { /* Diagnostics must not change recovery or cleanup. */ }
358
+ }
359
+ retire(s) {
360
+ if (this.candidates.get(s.binding.sessionId) === s)
361
+ this.candidates.delete(s.binding.sessionId);
362
+ s.retired = true;
363
+ s.inputQueue.length = 0;
364
+ s.abort.abort();
365
+ clearInterval(s.timer);
366
+ s.control?.close();
367
+ s.media?.close();
368
+ this.wakeMedia(s);
369
+ // Close native admission synchronously; waiting behind a dial/prepare must not
370
+ // postpone known-disconnect releases. Replacement still awaits this same result.
371
+ if (s.backend && !s.nativeCleanup) {
372
+ s.nativeCleanup = s.backend.stop();
373
+ void s.nativeCleanup.catch(() => { this.failedCleanup = true; });
374
+ }
375
+ }
376
+ cleanup(s) {
377
+ return s.cleanup ??= (async () => {
378
+ this.cleanupJobs++;
379
+ try {
380
+ this.retire(s);
381
+ try {
382
+ const preparing = s.preparation ? await s.preparation.catch(() => undefined) : undefined;
383
+ await (s.nativeCleanup ?? (s.backend ?? preparing)?.stop());
384
+ }
385
+ catch (error) {
386
+ this.failedCleanup = true;
387
+ throw error;
388
+ }
389
+ if (this.generation === s.binding.deviceGeneration && (s.backend || s.preparation))
390
+ this.options.refreshCapabilities?.(s.binding.deviceGeneration);
391
+ if (this.active === s)
392
+ this.active = undefined;
393
+ }
394
+ finally {
395
+ this.cleanupJobs--;
396
+ }
397
+ })();
398
+ }
399
+ async fail(s) {
400
+ if (!this.current(s))
401
+ return;
402
+ this.options.report(s.binding.deviceGeneration, { t: 'desktop_session_state', sessionId: s.binding.sessionId,
403
+ controlGeneration: s.binding.controlGeneration, state: 'FAILED', code: 'transport_failed' });
404
+ this.retire(s);
405
+ await this.cleanup(s);
406
+ }
407
+ async send(s, message) {
408
+ mediaRequire(this.current(s) && s.control);
409
+ await s.control.send(message);
410
+ }
411
+ async controlLoop(s) {
412
+ try {
413
+ for await (const message of s.control.messages) {
414
+ if (!this.current(s))
415
+ return;
416
+ mediaRequire(message.sessionId === s.binding.sessionId);
417
+ if (message.type === 'input' || message.type === 'held_state' || message.type === 'clipboard_request') {
418
+ this.scheduleInput(s, message);
419
+ continue;
420
+ }
421
+ mediaRequire(s.activeTasks < 32);
422
+ s.activeTasks++;
423
+ // Each request rechecks its captured session after awaits. Text polling/media drain
424
+ // cannot prevent release, held snapshots or authenticated renewal from being read.
425
+ const receiving = message.type === 'audio_select' || message.type === 'audio_ready' || message.type === 'audio_feedback'
426
+ ? (s.audioCommands = (s.audioCommands ?? Promise.resolve()).then(() => this.receive(s, message)))
427
+ : this.receive(s, message);
428
+ void receiving.catch(error => {
429
+ if (this.current(s))
430
+ this.diagnostic(s, 'control_receive', error, undefined, message.type);
431
+ return this.fail(s);
432
+ }).finally(() => { s.activeTasks--; });
433
+ }
434
+ }
435
+ catch (error) {
436
+ if (this.current(s))
437
+ this.diagnostic(s, 'control_stream', error);
438
+ }
439
+ if (this.current(s))
440
+ this.diagnostic(s, 'control_ended');
441
+ await this.fail(s);
442
+ }
443
+ scheduleInput(s, message) {
444
+ const last = s.inputQueue.at(-1)?.message;
445
+ const queued = { message, backend: s.backend, configured: s.configured, at: performance.now() };
446
+ // Replace only adjacent, unsent absolute positions in the same authority.
447
+ // Key/button/wheel/text, held snapshots and clipboard requests are barriers.
448
+ // One native operation is in flight: a delayed IPC reply cannot create an
449
+ // unbounded tail of obsolete positions or reorder a release behind a key.
450
+ if (last && absoluteMotion(last) && absoluteMotion(message) &&
451
+ last.payload.controlGeneration === message.payload.controlGeneration &&
452
+ 'displayRevision' in last.payload && 'displayRevision' in message.payload &&
453
+ last.payload.displayRevision === message.payload.displayRevision) {
454
+ s.inputQueue[s.inputQueue.length - 1] = queued;
455
+ }
456
+ else {
457
+ mediaRequire(s.inputQueue.length < 128);
458
+ s.inputQueue.push(queued);
459
+ }
460
+ if (s.inputRunning)
461
+ return;
462
+ s.inputRunning = true;
463
+ void (async () => {
464
+ try {
465
+ while (this.current(s) && s.inputQueue.length) {
466
+ const queued = s.inputQueue.shift();
467
+ if (queued.backend === s.backend && queued.configured === s.configured)
468
+ await this.receive(s, queued.message);
469
+ }
470
+ }
471
+ catch (error) {
472
+ if (this.current(s)) {
473
+ this.diagnostic(s, 'control_receive', error);
474
+ await this.fail(s);
475
+ }
476
+ }
477
+ finally {
478
+ s.inputRunning = false;
479
+ }
480
+ })();
481
+ }
482
+ async audioPackets(s, backend) {
483
+ if (!backend.audioPackets)
484
+ return;
485
+ try {
486
+ for await (const bytes of backend.audioPackets) {
487
+ if (!this.current(s) || s.backend !== backend)
488
+ return;
489
+ const packet = decodeAudio(bytes);
490
+ if (!s.audioReady || !s.audioConfig || packet.epoch !== s.audioConfig.audioEpoch)
491
+ continue;
492
+ const sendAudio = s.media?.sendAudio ?? s.control?.sendAudio;
493
+ if (sendAudio)
494
+ await bounded(sendAudio(bytes), 80, 'audio_stale');
495
+ }
496
+ }
497
+ catch {
498
+ if (this.current(s) && s.backend === backend && s.audioConfig && s.audioCommand) {
499
+ s.audioConfig = undefined;
500
+ await backend.stopAudio?.().catch(() => { });
501
+ await this.send(s, { v: 1, type: 'audio_state', sessionId: s.binding.sessionId,
502
+ payload: { commandId: s.audioCommand, state: 'unavailable', reason: 'audio_unavailable' } }).catch(() => { });
503
+ }
504
+ }
505
+ finally {
506
+ s.audioLoop = undefined;
507
+ }
508
+ }
509
+ async receive(s, message, resumeAudio = false) {
510
+ if (!this.current(s))
511
+ return;
512
+ if (resumeAudio && (message.type !== 'audio_select' || s.audioCommand !== message.payload.commandId || s.audioMode !== message.payload.mode))
513
+ return;
514
+ // A committed native resize can overtake already-forwarded control on the
515
+ // retained socket. Old geometry has no authority in the new epoch; discard
516
+ // it rather than retiring that healthy epoch or replaying held input there.
517
+ // Unbound/future geometry is also rejected without granting it authority or
518
+ // letting a single rejected command tear down the valid media session.
519
+ if (s.configured && (message.type === 'held_state' || message.type === 'input') &&
520
+ (message.payload.controlGeneration !== s.binding.controlGeneration ||
521
+ message.payload.displayRevision !== s.configured.displayRevision))
522
+ return;
523
+ if (message.type === 'focus' && s.configured &&
524
+ (message.payload.controlGeneration !== s.binding.controlGeneration ||
525
+ message.payload.mediaEpoch !== s.configured.mediaEpoch ||
526
+ message.payload.displayRevision !== s.configured.displayRevision))
527
+ return;
528
+ if (s.recovering && (message.type === 'held_state' || message.type === 'input')) {
529
+ mediaRequire(message.payload.controlGeneration === s.binding.controlGeneration &&
530
+ message.payload.displayRevision === s.configured?.displayRevision);
531
+ // The old helper is being stopped/verified. Valid messages already in flight
532
+ // cannot belong to the replacement and must never be replayed into it.
533
+ return;
534
+ }
535
+ if (message.type !== 'media_grant' && ['viewer_ready', 'decoder_ready', 'feedback', 'credit'].includes(message.type)) {
536
+ if (!s.backend || !s.configured)
537
+ return;
538
+ const epoch = 'epoch' in message.payload ? message.payload.epoch : 'mediaEpoch' in message.payload ? message.payload.mediaEpoch : undefined;
539
+ if (typeof epoch === 'number' && epoch < s.configured.mediaEpoch)
540
+ return;
541
+ }
542
+ if (message.type === 'media_grant') {
543
+ mediaRequire(message.controlGeneration === s.binding.controlGeneration && s.startCommand && !s.media && !s.mediaAttaching);
544
+ const attachment = Symbol('media_attachment'), backend = s.backend, configured = s.configured;
545
+ s.mediaAttaching = attachment;
546
+ s.flow = new DesktopMediaFlowWindow();
547
+ const bindingCurrent = () => this.current(s) && s.backend === backend && s.configured === configured;
548
+ const owns = () => bindingCurrent() && s.mediaAttaching === attachment;
549
+ let accepted;
550
+ const socketCurrent = () => accepted ? this.current(s) && s.media === accepted : owns();
551
+ try {
552
+ const media = await this.options.sockets.media(s.binding, message.grant, s.abort.signal, socketCurrent, message.quic);
553
+ if (!owns()) {
554
+ media.close();
555
+ return;
556
+ }
557
+ accepted = media;
558
+ s.media = media;
559
+ this.wakeMedia(s);
560
+ void media.closed?.then(() => {
561
+ if (this.current(s) && s.media === media) {
562
+ this.diagnostic(s, 'media_closed');
563
+ void this.recover(s);
564
+ }
565
+ });
566
+ if (media.cutEvents)
567
+ void this.cutEvents(s, media).catch(error => { if (this.current(s) && s.media === media) {
568
+ this.diagnostic(s, 'packet_loop', error);
569
+ void this.fail(s);
570
+ } });
571
+ await this.maybeStart(s);
572
+ }
573
+ catch (error) {
574
+ if (owns())
575
+ throw error;
576
+ }
577
+ finally {
578
+ if (s.mediaAttaching === attachment)
579
+ s.mediaAttaching = undefined;
580
+ }
581
+ return;
582
+ }
583
+ if (message.type === 'select' && this.options.newWorkAllowed?.() === false) {
584
+ await this.send(s, { v: 1, type: 'error', sessionId: s.binding.sessionId, payload: { code: 'busy', retryable: true } });
585
+ return;
586
+ }
587
+ if ('commandId' in message.payload && message.type !== 'audio_ready' && !message.type.startsWith('motion_cut_') && !(resumeAudio && message.type === 'audio_select')) {
588
+ const id = message.payload.commandId, body = canonicalDesktopJson(message);
589
+ const previous = s.commands.get(id);
590
+ if (previous) {
591
+ mediaRequire(previous === body);
592
+ return;
593
+ }
594
+ mediaRequire(s.commands.size < 1024);
595
+ s.commands.set(id, body);
596
+ }
597
+ switch (message.type) {
598
+ case 'motion_cut_prepare':
599
+ await this.prepareMotionCut(s, message.payload);
600
+ return;
601
+ case 'motion_cut_commit':
602
+ await this.commitMotionCut(s, message.payload);
603
+ return;
604
+ case 'cursor_visibility': {
605
+ let outcome = 'failed';
606
+ let fence = { mediaEpoch: 0, afterFrameId: 0 };
607
+ try {
608
+ if (s.backend?.setCursorVisible) {
609
+ fence = await s.backend.setCursorVisible(message.payload.visible);
610
+ outcome = 'applied';
611
+ }
612
+ }
613
+ catch { /* Report the failure without claiming visibility changed. */ }
614
+ if (this.current(s))
615
+ await this.send(s, { v: 1, type: 'cursor_visibility_result', sessionId: s.binding.sessionId, payload: { ...message.payload, outcome, ...fence } });
616
+ return;
617
+ }
618
+ case 'appearance': {
619
+ let outcome = 'failed';
620
+ try {
621
+ if (s.backend?.setAppearance) {
622
+ await s.backend.setAppearance(message.payload.colorScheme);
623
+ outcome = 'applied';
624
+ }
625
+ }
626
+ catch { /* An appearance failure must not retire the video/input session. */ }
627
+ if (this.current(s))
628
+ await this.send(s, { v: 1, type: 'appearance_result', sessionId: s.binding.sessionId,
629
+ payload: { ...message.payload, outcome } });
630
+ return;
631
+ }
632
+ case 'select': {
633
+ const operationCurrent = this.preparationCurrent(s.binding);
634
+ const busy = async () => {
635
+ if (s.commands.get(message.payload.commandId) === canonicalDesktopJson(message))
636
+ s.commands.delete(message.payload.commandId);
637
+ if (this.current(s))
638
+ await this.send(s, { v: 1, type: 'error', sessionId: s.binding.sessionId, payload: { code: 'busy', retryable: true } });
639
+ };
640
+ if (this.candidates.get(s.binding.sessionId) === s && !await this.enqueue(async () => {
641
+ mediaRequire(this.current(s));
642
+ if (!operationCurrent())
643
+ return false;
644
+ const previous = this.active;
645
+ if (previous) {
646
+ if (!operationCurrent(true))
647
+ return false;
648
+ this.retire(previous);
649
+ await this.cleanup(previous);
650
+ }
651
+ mediaRequire(this.current(s) && !this.failedCleanup);
652
+ if (!operationCurrent())
653
+ return false;
654
+ this.candidates.delete(s.binding.sessionId);
655
+ this.active = s;
656
+ s.binding.candidate = false;
657
+ return true;
658
+ })) {
659
+ await busy();
660
+ return;
661
+ }
662
+ if (!operationCurrent()) {
663
+ await busy();
664
+ return;
665
+ }
666
+ mediaRequire(!s.backend && !s.selection);
667
+ s.selection = message.payload;
668
+ s.configuredCommand = message.payload.commandId;
669
+ s.controller = new DesktopRateController({ fpsCeiling: message.payload.fpsCeiling, maxFps: message.payload.fpsCeiling });
670
+ s.preparation = this.options.provider.prepare(message.payload, s.binding, operationCurrent);
671
+ let backend;
672
+ try {
673
+ backend = await s.preparation;
674
+ }
675
+ catch (error) {
676
+ if (error instanceof NativeCommandError && error.code === 'busy') {
677
+ if (s.selection === message.payload) {
678
+ s.selection = undefined;
679
+ s.configuredCommand = undefined;
680
+ s.preparation = undefined;
681
+ }
682
+ await busy();
683
+ return;
684
+ }
685
+ throw error;
686
+ }
687
+ if (!this.current(s)) {
688
+ await backend.stop();
689
+ return;
690
+ }
691
+ s.backend = backend;
692
+ void this.events(s, backend);
693
+ return;
694
+ }
695
+ case 'audio_select': {
696
+ const command = message.payload, backend = s.backend;
697
+ s.audioCommand = command.commandId;
698
+ s.audioMode = command.mode;
699
+ s.audioPolicy ??= new AudioRateController();
700
+ const bitrate = s.audioPolicy.select(command.mode);
701
+ try {
702
+ if (command.mode === 'off') {
703
+ s.audioConfig = undefined;
704
+ s.audioFeedback = undefined;
705
+ s.audioReady = false;
706
+ s.control?.stopAudio?.();
707
+ await backend?.stopAudio?.();
708
+ if (this.current(s))
709
+ await this.send(s, { v: 1, type: 'audio_state', sessionId: s.binding.sessionId, payload: { commandId: command.commandId, state: 'off', reason: null } });
710
+ return;
711
+ }
712
+ if (!backend?.audioSupported || !backend.startAudio || !(s.media?.sendAudio ?? s.control?.sendAudio))
713
+ throw new NativeCommandError('audio_unavailable');
714
+ if (!s.media?.sendAudio)
715
+ await s.control?.prepareAudio?.();
716
+ if (!this.current(s) || s.audioCommand !== command.commandId)
717
+ return;
718
+ let configured = s.audioConfig;
719
+ if (configured) {
720
+ await backend.setAudioRate(bitrate);
721
+ configured = { ...configured, commandId: command.commandId, bitrate };
722
+ }
723
+ else {
724
+ s.audioReady = false;
725
+ const audioEpoch = (s.audioEpoch ?? 0) + 1;
726
+ s.audioEpoch = audioEpoch;
727
+ const native = await backend.startAudio(audioEpoch, bitrate);
728
+ configured = { ...native, commandId: command.commandId };
729
+ }
730
+ if (!this.current(s) || s.backend !== backend || s.audioCommand !== command.commandId)
731
+ return;
732
+ s.audioConfig = configured;
733
+ s.audioFeedback = undefined;
734
+ await this.send(s, { v: 1, type: 'audio_configured', sessionId: s.binding.sessionId, payload: configured });
735
+ await this.send(s, { v: 1, type: 'audio_state', sessionId: s.binding.sessionId, payload: { commandId: command.commandId, state: 'buffering', reason: null } });
736
+ s.audioLoop ??= this.audioPackets(s, backend);
737
+ }
738
+ catch {
739
+ s.audioConfig = undefined;
740
+ await backend?.stopAudio?.().catch(() => { });
741
+ if (this.current(s))
742
+ await this.send(s, { v: 1, type: 'audio_state', sessionId: s.binding.sessionId, payload: { commandId: command.commandId, state: 'unavailable', reason: 'audio_unavailable' } });
743
+ }
744
+ return;
745
+ }
746
+ case 'audio_ready': {
747
+ if (s.audioCommand === message.payload.commandId && s.audioConfig?.audioEpoch === message.payload.audioEpoch)
748
+ s.audioReady = true;
749
+ return;
750
+ }
751
+ case 'audio_feedback': {
752
+ const value = message.payload, previous = s.audioFeedback;
753
+ if (!s.audioConfig || value.audioEpoch !== s.audioConfig.audioEpoch || !s.audioPolicy)
754
+ return;
755
+ if (!previous) {
756
+ s.audioFeedback = value;
757
+ return;
758
+ }
759
+ if (value.receivedPackets < previous.receivedPackets || value.lostPackets < previous.lostPackets ||
760
+ value.latePackets < previous.latePackets || value.underruns < previous.underruns)
761
+ return;
762
+ s.audioFeedback = value;
763
+ const received = Math.max(0, value.receivedPackets - previous.receivedPackets), lost = Math.max(0, value.lostPackets - previous.lostPackets);
764
+ const late = Math.max(0, value.latePackets - previous.latePackets), total = received + lost;
765
+ const network = s.controller.diagnostics.networkState;
766
+ const bitrate = s.audioPolicy.observe(performance.now(), { queueMs: value.queueAgeMs, lateRatio: total ? late / total : 0,
767
+ lostRatio: total ? lost / total : 0, underruns: Math.max(0, value.underruns - previous.underruns),
768
+ congested: network === 'congested', headroom: network === 'usable' && total > 0 && lost === 0 && late === 0 });
769
+ if (bitrate !== s.audioConfig.bitrate && s.backend?.setAudioRate) {
770
+ await s.backend.setAudioRate(bitrate);
771
+ s.audioConfig = { ...s.audioConfig, bitrate };
772
+ await this.send(s, { v: 1, type: 'audio_configured', sessionId: s.binding.sessionId, payload: s.audioConfig });
773
+ }
774
+ return;
775
+ }
776
+ case 'viewer_ready': {
777
+ this.ready(s, message.payload);
778
+ s.viewerReady = true;
779
+ await this.maybeStart(s);
780
+ return;
781
+ }
782
+ case 'start': {
783
+ mediaRequire(s.configured && s.viewerReady);
784
+ s.startCommand = message.payload.commandId;
785
+ await this.maybeStart(s);
786
+ return;
787
+ }
788
+ case 'credit': {
789
+ // The relay's initial credit can overtake viewer readiness and START on
790
+ // the retained control socket. This only funds the bound transport hop;
791
+ // returnNativeCredit still requires this epoch's actual capture START.
792
+ mediaRequire(s.configured && message.payload.epoch === s.configured.mediaEpoch &&
793
+ (s.media || s.mediaAttaching));
794
+ s.flow.credit(message.payload);
795
+ s.lastPacketAt = performance.now();
796
+ this.wakeMedia(s);
797
+ this.returnNativeCredit(s);
798
+ return;
799
+ }
800
+ case 'decoder_ready': {
801
+ this.ready(s, message.payload);
802
+ mediaRequire(s.backend && s.codec && s.startedEpoch === s.configured.mediaEpoch && s.media);
803
+ const backend = s.backend, configured = s.configured;
804
+ s.decoderAcknowledgement = backend.decoderReady({ ...message.payload, representation: s.codec.representation, parameterSets: s.codec.parameterSets });
805
+ await s.decoderAcknowledgement;
806
+ if (this.current(s) && s.backend === backend && s.configured === configured)
807
+ s.decoderReady = true;
808
+ return;
809
+ }
810
+ case 'feedback': {
811
+ const f = message.payload;
812
+ // Feedback is an observation, not authority to retire capture or input.
813
+ // A fast receiver can report before send() settles; old observations can
814
+ // also cross a configuration/cut. Leave accepted counters intact and wait
815
+ // for the next current observation instead of black-screening the owner.
816
+ if (!(s.backend && s.configured && f.epoch === s.configured.mediaEpoch &&
817
+ f.presentedId <= s.receivedFrame && f.receivedId <= s.receivedFrame && f.receivedTotalBytes >= s.lastFeedbackBytes &&
818
+ (!s.feedback || f.presentedId >= s.feedback.presentedId && f.decodedId >= s.feedback.decodedId && f.receivedId >= s.feedback.receivedId && f.exactPresentedSequence >= s.feedback.exactPresentedSequence)))
819
+ return;
820
+ const previousExactPresented = s.feedback?.exactPresentedSequence ?? 0;
821
+ const backend = s.backend, configured = s.configured;
822
+ const observedAt = performance.now();
823
+ if (f.receivedTotalBytes > s.lastFeedbackBytes || f.receivedId > (s.feedback?.receivedId ?? 0))
824
+ s.feedbackProgressAt = observedAt;
825
+ if (f.decodedId > (s.feedback?.decodedId ?? 0))
826
+ s.decodeObservedAt = observedAt;
827
+ s.feedback = f;
828
+ s.lastFeedbackBytes = f.receivedTotalBytes;
829
+ s.delivery.received(f.receivedId, performance.now());
830
+ if (s.firstCompleteFrame > 0 && f.presentedId >= s.firstCompleteFrame && !s.presented) {
831
+ const backend = s.backend, configured = s.configured;
832
+ mediaRequire(s.decoderReady);
833
+ const acknowledgement = s.presentationAcknowledgement ??= backend.firstPresented({ epoch: f.epoch, frameId: f.presentedId });
834
+ try {
835
+ await acknowledgement;
836
+ }
837
+ catch (error) {
838
+ if (this.current(s) && s.backend === backend && s.configured === configured)
839
+ throw error;
840
+ return;
841
+ }
842
+ if (this.current(s) && s.backend === backend && s.configured === configured) {
843
+ s.presented = true;
844
+ // The Server has committed this geometry; publishing during negotiation
845
+ // would invalidate the capability snapshot that authorized the attempt.
846
+ this.options.refreshCapabilities?.(s.binding.deviceGeneration);
847
+ }
848
+ }
849
+ if (f.exactPresentedSequence > previousExactPresented && this.current(s) && s.backend === backend &&
850
+ s.configured === configured && s.presented) {
851
+ await backend.acknowledge?.({ epoch: f.epoch, exactSequence: f.exactPresentedSequence });
852
+ }
853
+ return;
854
+ }
855
+ case 'clipboard_request': {
856
+ mediaRequire(s.backend?.clipboard && message.payload.controlGeneration === s.binding.controlGeneration);
857
+ const backend = s.backend, configured = s.configured;
858
+ if (!s.presented) {
859
+ const acknowledgement = s.presentationAcknowledgement;
860
+ mediaRequire(acknowledgement);
861
+ try {
862
+ await acknowledgement;
863
+ }
864
+ catch (error) {
865
+ if (this.current(s) && s.backend === backend && s.configured === configured &&
866
+ s.presentationAcknowledgement === acknowledgement)
867
+ throw error;
868
+ return;
869
+ }
870
+ if (!this.current(s) || s.backend !== backend || s.configured !== configured ||
871
+ s.presentationAcknowledgement !== acknowledgement)
872
+ return;
873
+ mediaRequire(s.presented);
874
+ }
875
+ const result = await backend.clipboard(message.payload);
876
+ if (this.current(s) && s.backend === backend && s.configured === configured)
877
+ await this.send(s, { v: 1, type: 'clipboard_result', sessionId: s.binding.sessionId, payload: result });
878
+ return;
879
+ }
880
+ case 'input': {
881
+ const backend = s.backend, configured = s.configured;
882
+ mediaRequire(backend);
883
+ if (!s.presented && message.payload.action !== 'release_all') {
884
+ // Feedback already arrived on this ordered socket; its native commit
885
+ // may still be awaiting IPC. Never admit before that exact commit.
886
+ const acknowledgement = s.presentationAcknowledgement;
887
+ mediaRequire(acknowledgement);
888
+ try {
889
+ await acknowledgement;
890
+ }
891
+ catch (error) {
892
+ if (this.current(s) && s.backend === backend && s.configured === configured &&
893
+ s.presentationAcknowledgement === acknowledgement)
894
+ throw error;
895
+ return;
896
+ }
897
+ if (!this.current(s) || s.backend !== backend || s.configured !== configured ||
898
+ s.presentationAcknowledgement !== acknowledgement)
899
+ return;
900
+ mediaRequire(s.presented);
901
+ }
902
+ const result = await backend.input(message.payload);
903
+ if (this.current(s) && s.backend === backend && s.configured === configured)
904
+ await this.send(s, { v: 1, type: 'input_result', sessionId: s.binding.sessionId, payload: result });
905
+ return;
906
+ }
907
+ case 'held_state': {
908
+ mediaRequire(s.backend && message.payload.controlGeneration === s.binding.controlGeneration && message.payload.displayRevision === s.configured?.displayRevision);
909
+ await s.backend.heldState(message.payload);
910
+ return;
911
+ }
912
+ case 'pong': {
913
+ const challenge = s.challenges.get(message.payload.challenge);
914
+ if (challenge) {
915
+ if (!challenge.answered) {
916
+ challenge.answered = true;
917
+ s.rtt = Math.max(0, performance.now() - challenge.at);
918
+ s.rtts.push(s.rtt);
919
+ if (s.rtts.length > 60)
920
+ s.rtts.shift();
921
+ }
922
+ }
923
+ else if (s.backend)
924
+ await s.backend.renewAuthenticatedLease({ leaseMs: 30000 });
925
+ return;
926
+ }
927
+ case 'ping': {
928
+ await this.send(s, { ...message, type: 'pong' });
929
+ return;
930
+ }
931
+ case 'cache_dependency': {
932
+ const request = message.payload, backend = s.backend, configured = s.configured;
933
+ mediaRequire(backend?.cacheDependency && backend.reserveOverview && request.controlGeneration === s.binding.controlGeneration &&
934
+ request.mediaEpoch === configured?.mediaEpoch && request.displayRevision === configured.displayRevision);
935
+ let pixels;
936
+ try {
937
+ pixels = await backend.cacheDependency(request);
938
+ }
939
+ catch (error) {
940
+ if (!this.current(s) || s.backend !== backend || s.configured !== configured || backend.epoch !== undefined && backend.epoch !== request.mediaEpoch)
941
+ return;
942
+ throw error;
943
+ }
944
+ const result = { v: 1, type: 'cache_dependency_result', sessionId: s.binding.sessionId, payload: { request, pixels } };
945
+ const wireBytes = Buffer.byteLength(encodeDesktopControl(result)) + 8, deadline = performance.now() + 4000;
946
+ while (this.current(s) && s.backend === backend && s.configured === configured && (backend.epoch === undefined || backend.epoch === request.mediaEpoch)) {
947
+ if (await backend.reserveOverview({ epoch: request.mediaEpoch, wireBytes })) {
948
+ if (this.current(s) && s.backend === backend && s.configured === configured)
949
+ await this.send(s, result);
950
+ return;
951
+ }
952
+ mediaRequire(performance.now() < deadline);
953
+ await dependencyDelay(10);
954
+ }
955
+ return;
956
+ }
957
+ case 'focus': {
958
+ const request = message.payload;
959
+ mediaRequire(s.backend?.focus && request.controlGeneration === s.binding.controlGeneration &&
960
+ request.mediaEpoch === s.configured?.mediaEpoch && request.displayRevision === s.configured.displayRevision);
961
+ const backend = s.backend, configured = s.configured;
962
+ if (!s.presented) {
963
+ const acknowledgement = s.presentationAcknowledgement;
964
+ // Navigation crossing a configuration is obsolete. A valid first-frame
965
+ // acknowledgement already in flight, however, precedes this command.
966
+ if (!acknowledgement)
967
+ return;
968
+ try {
969
+ await acknowledgement;
970
+ }
971
+ catch (error) {
972
+ if (this.current(s) && s.backend === backend && s.configured === configured)
973
+ throw error;
974
+ return;
975
+ }
976
+ if (!this.current(s) || s.backend !== backend || s.configured !== configured ||
977
+ s.presentationAcknowledgement !== acknowledgement || !s.presented)
978
+ return;
979
+ }
980
+ const owns = () => this.current(s) && s.backend === backend && s.configured === configured &&
981
+ (backend.epoch === undefined || backend.epoch === request.mediaEpoch);
982
+ let result;
983
+ try {
984
+ result = await backend.focus(request.focus);
985
+ }
986
+ catch (error) {
987
+ if (owns())
988
+ throw error;
989
+ return;
990
+ }
991
+ if (owns())
992
+ await this.send(s, { v: 1, type: 'focus_result', sessionId: s.binding.sessionId, payload: { ...request, ...result } });
993
+ return;
994
+ }
995
+ case 'resize':
996
+ case 'request_refresh': {
997
+ mediaRequire(s.backend);
998
+ // A viewer command may cross an automatic media_open already in flight.
999
+ // The viewer retains its choice and retries after that epoch is presented.
1000
+ if (s.automaticConfiguration)
1001
+ return;
1002
+ if (!s.presented && s.presentationAcknowledgement) {
1003
+ const backend = s.backend, configured = s.configured, acknowledgement = s.presentationAcknowledgement;
1004
+ try {
1005
+ await acknowledgement;
1006
+ }
1007
+ catch (error) {
1008
+ if (this.current(s) && s.backend === backend && s.configured === configured &&
1009
+ s.presentationAcknowledgement === acknowledgement)
1010
+ throw error;
1011
+ return;
1012
+ }
1013
+ if (!this.current(s) || s.backend !== backend || s.configured !== configured ||
1014
+ s.presentationAcknowledgement !== acknowledgement)
1015
+ return;
1016
+ }
1017
+ if (!s.presented)
1018
+ return;
1019
+ // The old stream must finish its credited writes before native can commit
1020
+ // the new configuration. Input closes now; decoder/START retire at CONFIG.
1021
+ s.viewerReady = false;
1022
+ s.presented = false;
1023
+ s.configuredCommand = message.payload.commandId;
1024
+ s.backend?.configurationCommand?.(message.payload.commandId);
1025
+ if (message.type === 'resize') {
1026
+ mediaRequire(message.payload.expectedDisplayRevision === s.configured?.displayRevision);
1027
+ const result = await s.backend.resize({ width: message.payload.width, height: message.payload.height,
1028
+ ...(message.payload.dpi === undefined ? {} : { dpi: message.payload.dpi }),
1029
+ ...(message.payload.captureRegion === undefined ? {} : { captureRegion: message.payload.captureRegion }) });
1030
+ if (this.current(s))
1031
+ await this.send(s, { v: 1, type: 'resize_result', sessionId: s.binding.sessionId, payload: { ...result, commandId: message.payload.commandId } });
1032
+ }
1033
+ else
1034
+ await s.backend.refresh();
1035
+ return;
1036
+ }
1037
+ case 'stop':
1038
+ case 'suspend':
1039
+ case 'resume': {
1040
+ this.retire(s);
1041
+ await this.cleanup(s);
1042
+ return;
1043
+ }
1044
+ case 'error': {
1045
+ this.diagnostic(s, 'viewer_error');
1046
+ await this.fail(s);
1047
+ return;
1048
+ }
1049
+ case 'state': {
1050
+ if (['FAILED', 'CLOSED', 'SUSPENDED'].includes(message.payload.state)) {
1051
+ this.diagnostic(s, 'viewer_state');
1052
+ this.retire(s);
1053
+ await this.cleanup(s);
1054
+ }
1055
+ return;
1056
+ }
1057
+ default: throw new Error('invalid_request');
1058
+ }
1059
+ }
1060
+ ready(s, value) {
1061
+ mediaRequire(s.configured && value.controlGeneration === s.binding.controlGeneration && value.mediaEpoch === s.configured.mediaEpoch && value.displayRevision === s.configured.displayRevision);
1062
+ }
1063
+ async maybeStart(s) {
1064
+ if (!this.current(s) || !s.backend || !s.configured || !s.viewerReady || !s.startCommand || !s.media || s.starting || s.startedEpoch === s.configured.mediaEpoch)
1065
+ return;
1066
+ const configured = s.configured, epoch = configured.mediaEpoch, backend = s.backend;
1067
+ if (backend.epoch !== undefined && backend.epoch !== epoch)
1068
+ return;
1069
+ s.starting = true;
1070
+ // START is authorized. The persistent reader can receive CONFIG before
1071
+ // the helper acknowledges start, so publish the permitted epoch first.
1072
+ s.startedEpoch = epoch;
1073
+ try {
1074
+ await backend.start();
1075
+ if (!this.current(s) || s.backend !== backend || s.configured !== configured)
1076
+ return;
1077
+ s.startedEpoch = epoch;
1078
+ // Independent bounded hop: grant one initial native window, then replenish each
1079
+ // fragment only after the actual device WS send callback, never after decode/present.
1080
+ this.returnNativeCredit(s);
1081
+ if (!this.current(s) || s.backend !== backend || s.configured !== configured || s.startedEpoch !== epoch)
1082
+ return;
1083
+ s.lastPacketAt = performance.now();
1084
+ if (!s.packetLoop) {
1085
+ s.packetLoop = true;
1086
+ void this.packets(s);
1087
+ }
1088
+ if (s.audioMode && s.audioMode !== 'off' && s.audioCommand && !s.audioConfig) {
1089
+ const mode = s.audioMode, commandId = s.audioCommand;
1090
+ s.audioCommands = (s.audioCommands ?? Promise.resolve()).then(() => this.receive(s, { v: 1, type: 'audio_select',
1091
+ sessionId: s.binding.sessionId, payload: { commandId, mode } }, true)).catch(() => { });
1092
+ }
1093
+ }
1094
+ catch (error) {
1095
+ if (this.current(s) && s.backend === backend && s.configured === configured)
1096
+ throw error;
1097
+ }
1098
+ finally {
1099
+ if (s.backend === backend && s.configured === configured)
1100
+ s.starting = false;
1101
+ }
1102
+ }
1103
+ async events(s, backend) {
1104
+ try {
1105
+ for await (const event of backend.events) {
1106
+ if (!this.current(s) || s.backend !== backend)
1107
+ return;
1108
+ if (event.type === 'configured') {
1109
+ const cfg = event.configured;
1110
+ mediaRequire(cfg.deviceGeneration === s.binding.deviceGeneration && cfg.controlGeneration === s.binding.controlGeneration && cfg.commandId === s.configuredCommand);
1111
+ if (s.automaticConfiguration === cfg.commandId) {
1112
+ await this.send(s, { v: 1, type: 'media_open', sessionId: s.binding.sessionId, payload: { commandId: cfg.commandId } });
1113
+ if (!this.current(s) || s.backend !== backend)
1114
+ return;
1115
+ const oldMedia = s.media;
1116
+ s.media = undefined;
1117
+ oldMedia?.close();
1118
+ s.startCommand = undefined;
1119
+ s.automaticConfiguration = undefined;
1120
+ }
1121
+ s.configured = cfg;
1122
+ s.codec = undefined;
1123
+ s.viewerReady = false;
1124
+ s.decoderReady = false;
1125
+ s.decoderAcknowledgement = undefined;
1126
+ s.presented = false;
1127
+ if (s.selection)
1128
+ s.selection = { ...s.selection, captureRegion: cfg.captureRegion, width: cfg.width, height: cfg.height,
1129
+ ...(cfg.dpi === undefined ? {} : { dpi: cfg.dpi }) };
1130
+ s.startCommand = undefined;
1131
+ s.mediaAttaching = undefined;
1132
+ s.startedEpoch = 0;
1133
+ s.starting = false;
1134
+ s.firstCompleteFrame = 0;
1135
+ s.receivedFrame = 0;
1136
+ s.presentationAcknowledgement = undefined;
1137
+ s.flow = new DesktopMediaFlowWindow();
1138
+ s.configAdmittedEpoch = 0;
1139
+ s.nativeAdvertised = undefined;
1140
+ s.nativeDisposed = { motion: 0, cursor: 0, exact: 0 };
1141
+ s.motionCut = undefined;
1142
+ s.cutRequest = undefined;
1143
+ s.untrustedCut = undefined;
1144
+ this.wakeMedia(s);
1145
+ // Picture and exact presentation watermarks belong to this media epoch.
1146
+ // Keep lastFeedbackBytes: receivedTotalBytes is cumulative for the connection.
1147
+ s.feedback = undefined;
1148
+ s.feedbackProgressAt = undefined;
1149
+ s.decodeObservedAt = undefined;
1150
+ s.motionEncodeObservedAt = undefined;
1151
+ s.motionEncodeKey = undefined;
1152
+ s.motionEncodeMs = 0;
1153
+ s.delivery = new DesktopDeliveryDelay();
1154
+ s.capturePaused = false;
1155
+ s.recovering = false;
1156
+ await this.send(s, { v: 1, type: 'configured', sessionId: s.binding.sessionId, payload: cfg });
1157
+ }
1158
+ else if (event.type === 'failed') {
1159
+ this.diagnostic(s, 'backend_failed', new NativeCommandError(event.code));
1160
+ await this.recover(s);
1161
+ }
1162
+ else if (event.type === 'recovery' && !event.result.verified) {
1163
+ this.diagnostic(s, 'backend_recovery_unverified');
1164
+ await this.fail(s);
1165
+ }
1166
+ }
1167
+ }
1168
+ catch (error) {
1169
+ if (this.current(s) && s.backend === backend) {
1170
+ this.diagnostic(s, 'backend_events', error);
1171
+ await this.fail(s);
1172
+ }
1173
+ }
1174
+ }
1175
+ cutBinding(s, p) {
1176
+ return this.current(s) && p.controlGeneration === s.binding.controlGeneration && p.mediaEpoch === s.configured?.mediaEpoch && p.displayRevision === s.configured.displayRevision && s.startedEpoch === p.mediaEpoch && !s.recovering;
1177
+ }
1178
+ async cutEvents(s, media) {
1179
+ for await (const event of media.cutEvents) {
1180
+ if (!this.current(s) || s.media !== media)
1181
+ return;
1182
+ if (event.epoch < (s.configured?.mediaEpoch ?? 0))
1183
+ continue;
1184
+ mediaRequire(event.epoch === s.configured?.mediaEpoch);
1185
+ if (event.action === 1) {
1186
+ if (s.motionCut?.phase === 'committed' && event.cutId < s.motionCut.payload.cutId)
1187
+ continue;
1188
+ if (s.presented && s.decoderReady && !s.recovering && !s.cutRequest && (!s.motionCut || s.motionCut.phase === 'committed')) {
1189
+ const commandId = randomBytes(16).toString('hex');
1190
+ s.cutRequest = commandId;
1191
+ await this.send(s, { v: 1, type: 'motion_cut_request', sessionId: s.binding.sessionId, payload: { commandId, controlGeneration: s.binding.controlGeneration, mediaEpoch: event.epoch, displayRevision: s.configured.displayRevision } });
1192
+ }
1193
+ }
1194
+ else if (event.action === 2) {
1195
+ // A transport proposal is untrusted until the coordinator's matching control transaction.
1196
+ mediaRequire(!s.untrustedCut || canonicalDesktopJson(s.untrustedCut) === canonicalDesktopJson(event));
1197
+ s.untrustedCut = event;
1198
+ }
1199
+ else if (event.action === 4 || event.action === 5) {
1200
+ const cut = s.motionCut;
1201
+ mediaRequire(cut && cut.fence && event.cutId === cut.payload.cutId && event.epoch === cut.payload.mediaEpoch && event.pictureThrough === cut.fence.pictureThrough && event.frameThrough === cut.fence.frameThrough);
1202
+ if (event.action === 4)
1203
+ cut.acked = true;
1204
+ else {
1205
+ mediaRequire(cut.phase === 'committing' && cut.acked);
1206
+ cut.resumed = true;
1207
+ }
1208
+ this.wakeMedia(s);
1209
+ }
1210
+ else if (event.action === 6)
1211
+ throw new NativeCommandError('transport_failed');
1212
+ }
1213
+ }
1214
+ async prepareMotionCut(s, payload) {
1215
+ mediaRequire(this.cutBinding(s, payload) && s.presented && s.backend?.prepareMotionCut && s.backend.settleMotionCut && s.backend.mediaDisposed && s.backend.commitMotionCut && s.media);
1216
+ let cut = s.motionCut;
1217
+ if (cut && cut.payload.cutId === payload.cutId) {
1218
+ mediaRequire(canonicalDesktopJson(cut.payload) === canonicalDesktopJson(payload));
1219
+ if (cut.preparing)
1220
+ await cut.preparing;
1221
+ }
1222
+ else {
1223
+ mediaRequire(!cut || cut.phase === 'committed' && payload.cutId > cut.payload.cutId);
1224
+ cut = { payload, phase: 'preparing', acked: false, resumed: false };
1225
+ s.motionCut = cut;
1226
+ this.wakeMedia(s);
1227
+ const owner = cut, backend = s.backend, media = s.media;
1228
+ cut.preparing = (async () => {
1229
+ const fence = await backend.prepareMotionCut({ epoch: payload.mediaEpoch, cutId: payload.cutId });
1230
+ mediaRequire(this.cutBinding(s, payload) && s.backend === backend && s.media === media && s.motionCut === owner && fence.epoch === payload.mediaEpoch && fence.cutId === payload.cutId);
1231
+ owner.fence = fence;
1232
+ this.wakeMedia(s);
1233
+ if (media.cutCommand)
1234
+ await media.cutCommand(9, payload.cutId, payload.mediaEpoch, fence.pictureThrough, fence.frameThrough);
1235
+ await backend.settleMotionCut(fence);
1236
+ mediaRequire(this.cutBinding(s, payload) && s.backend === backend && s.media === media && s.motionCut === owner);
1237
+ if (!media.cutCommand) {
1238
+ mediaRequire(media.sendCutBarrier);
1239
+ await media.sendCutBarrier({ v: 1, type: 'motion_cut_barrier', sessionId: s.binding.sessionId, payload: { ...payload, pictureThrough: fence.pictureThrough, frameThrough: fence.frameThrough } });
1240
+ }
1241
+ owner.phase = 'prepared';
1242
+ })();
1243
+ await cut.preparing;
1244
+ cut.preparing = undefined;
1245
+ }
1246
+ mediaRequire(this.cutBinding(s, payload) && cut.fence);
1247
+ await this.send(s, { v: 1, type: 'motion_cut_prepared', sessionId: s.binding.sessionId, payload: { ...payload, pictureThrough: cut.fence.pictureThrough, frameThrough: cut.fence.frameThrough } });
1248
+ }
1249
+ async commitMotionCut(s, payload) {
1250
+ const cut = s.motionCut, backend = s.backend, media = s.media;
1251
+ mediaRequire(this.cutBinding(s, payload) && cut && canonicalDesktopJson(cut.payload) === canonicalDesktopJson(payload) && backend?.commitMotionCut && media);
1252
+ if (cut.phase === 'committed')
1253
+ return;
1254
+ if (cut.committing) {
1255
+ await cut.committing;
1256
+ return;
1257
+ }
1258
+ cut.committing = (async () => {
1259
+ if (cut.preparing)
1260
+ await cut.preparing;
1261
+ mediaRequire(cut.phase === 'prepared');
1262
+ cut.phase = 'committing';
1263
+ if (media.cutCommand) {
1264
+ while (!cut.acked) {
1265
+ mediaRequire(this.cutBinding(s, payload) && s.media === media);
1266
+ await this.waitMedia(s);
1267
+ }
1268
+ await media.cutCommand(11, payload.cutId, payload.mediaEpoch);
1269
+ while (!cut.resumed) {
1270
+ mediaRequire(this.cutBinding(s, payload) && s.media === media);
1271
+ await this.waitMedia(s);
1272
+ }
1273
+ }
1274
+ mediaRequire(this.cutBinding(s, payload) && s.backend === backend && s.media === media);
1275
+ await backend.commitMotionCut({ epoch: payload.mediaEpoch, cutId: payload.cutId });
1276
+ mediaRequire(this.cutBinding(s, payload) && s.backend === backend && s.media === media);
1277
+ media.finishCut?.(payload.cutId);
1278
+ cut.phase = 'committed';
1279
+ s.cutRequest = undefined;
1280
+ s.untrustedCut = undefined;
1281
+ this.wakeMedia(s);
1282
+ })();
1283
+ await cut.committing;
1284
+ }
1285
+ disposeMotion(s, backend, packet, cut) {
1286
+ mediaRequire(cut.fence && packet.type === 2 && packet.epoch === cut.payload.mediaEpoch && packet.sequence <= cut.fence.pictureThrough && backend.mediaDisposed);
1287
+ backend.mediaDisposed(packet, cut.fence);
1288
+ s.nativeDisposed.motion += packet.payload.length;
1289
+ mediaRequire(Number.isSafeInteger(s.nativeDisposed.motion));
1290
+ this.returnNativeCredit(s);
1291
+ this.wakeMedia(s);
1292
+ }
1293
+ wakeMedia(s) { for (const wake of [...s.mediaWake])
1294
+ wake(); }
1295
+ async waitMedia(s) {
1296
+ let wake;
1297
+ const waiting = new Promise(resolve => { wake = resolve; s.mediaWake.add(wake); });
1298
+ try {
1299
+ await waiting;
1300
+ }
1301
+ finally {
1302
+ s.mediaWake.delete(wake);
1303
+ }
1304
+ }
1305
+ returnNativeCredit(s) {
1306
+ s.nativeCreditDirty = true;
1307
+ if (s.nativeCreditRunning)
1308
+ return;
1309
+ const task = (async () => {
1310
+ while (this.current(s) && s.nativeCreditDirty) {
1311
+ s.nativeCreditDirty = false;
1312
+ const backend = s.backend, configured = s.configured, flow = s.flow;
1313
+ if (!backend?.credit || !configured || s.startedEpoch !== configured.mediaEpoch || !flow.hasCredit)
1314
+ continue;
1315
+ const value = { epoch: configured.mediaEpoch, ...flow.appliedCredit };
1316
+ value.motionConsumedBytes += s.nativeDisposed.motion;
1317
+ value.cursorConsumedBytes += s.nativeDisposed.cursor;
1318
+ value.exactConsumedBytes += s.nativeDisposed.exact;
1319
+ if (s.nativeAdvertised && canonicalDesktopJson(s.nativeAdvertised) === canonicalDesktopJson(value))
1320
+ continue;
1321
+ try {
1322
+ await backend.credit(value);
1323
+ }
1324
+ catch (error) {
1325
+ if (this.current(s) && s.backend === backend && s.configured === configured) {
1326
+ this.diagnostic(s, 'packet_loop', error, { stage: 'native_credit', type: 0, sequence: 0, payloadBytes: 0 });
1327
+ await this.recover(s);
1328
+ }
1329
+ return;
1330
+ }
1331
+ if (this.current(s) && s.backend === backend && s.configured === configured && s.flow === flow)
1332
+ s.nativeAdvertised = value;
1333
+ }
1334
+ })();
1335
+ s.nativeCreditRunning = task;
1336
+ void task.finally(() => { if (s.nativeCreditRunning === task) {
1337
+ s.nativeCreditRunning = undefined;
1338
+ if (s.nativeCreditDirty && this.current(s))
1339
+ this.returnNativeCredit(s);
1340
+ } }).catch(() => { });
1341
+ }
1342
+ async packets(s) {
1343
+ const backend = s.backend;
1344
+ const queues = { motion: [], cursor: [], exact: [] };
1345
+ const active = new Map();
1346
+ let queuedBytes = 0, ended = false, failed = false, nextId = 0;
1347
+ const current = () => this.current(s) && s.backend === backend;
1348
+ const queuedCount = () => queues.motion.length + queues.cursor.length + queues.exact.length;
1349
+ const updateAge = () => { if (!current())
1350
+ return; s.queuedAt = Math.min(...[...active.values(), ...queues.motion, ...queues.cursor, ...queues.exact].map(r => r.at), Infinity); if (s.queuedAt === Infinity)
1351
+ s.queuedAt = 0; };
1352
+ const fail = async (error, p, stage = 'native_read') => {
1353
+ if (failed || !current())
1354
+ return;
1355
+ failed = true;
1356
+ ended = true;
1357
+ this.wakeMedia(s);
1358
+ this.diagnostic(s, 'packet_loop', error, { stage, type: p?.type ?? 0, sequence: p?.sequence ?? 0, payloadBytes: p?.payload.length ?? 0 });
1359
+ await this.recover(s);
1360
+ };
1361
+ const worker = async (lane) => {
1362
+ let packet, stage = 'native_read';
1363
+ try {
1364
+ while (current() && !failed) {
1365
+ const record = queues[lane][0];
1366
+ if (!record) {
1367
+ if (ended)
1368
+ return;
1369
+ await this.waitMedia(s);
1370
+ continue;
1371
+ }
1372
+ packet = record.p;
1373
+ if (lane === 'motion' && packet.type === 2) {
1374
+ while (current() && !failed && s.motionCut?.phase === 'preparing' && !s.motionCut.fence)
1375
+ await this.waitMedia(s);
1376
+ const cut = s.motionCut;
1377
+ if (cut?.fence && packet.epoch === cut.payload.mediaEpoch && packet.sequence <= cut.fence.pictureThrough) {
1378
+ queues[lane].shift();
1379
+ queuedBytes -= packet.payload.length;
1380
+ this.disposeMotion(s, backend, packet, cut);
1381
+ updateAge();
1382
+ continue;
1383
+ }
1384
+ }
1385
+ const configured = s.configured, flow = s.flow;
1386
+ mediaRequire(configured && packet.epoch === configured.mediaEpoch && packet.displayRevision === configured.displayRevision && s.startedEpoch === packet.epoch);
1387
+ if (packet.type !== 1) {
1388
+ while (current() && !failed && s.configAdmittedEpoch !== packet.epoch)
1389
+ await this.waitMedia(s);
1390
+ stage = 'decoder_wait';
1391
+ if ((packet.type === 2 || packet.type === 5) && !s.decoderReady && s.decoderAcknowledgement)
1392
+ await s.decoderAcknowledgement;
1393
+ mediaRequire(s.codec && ((packet.type !== 2 && packet.type !== 5) || s.decoderReady));
1394
+ }
1395
+ stage = 'credit_wait';
1396
+ while (current() && !failed && (!s.media || !flow.admit(lane, packet.payload.length)))
1397
+ await this.waitMedia(s);
1398
+ if (!current() || failed)
1399
+ return;
1400
+ const media = s.media;
1401
+ mediaRequire(media && s.configured === configured && s.flow === flow);
1402
+ const id = ++nextId;
1403
+ mediaRequire(flow.reserve(id, lane, packet.payload.length));
1404
+ queues[lane].shift();
1405
+ queuedBytes -= packet.payload.length;
1406
+ active.set(lane, record);
1407
+ updateAge();
1408
+ stage = 'send';
1409
+ try {
1410
+ await media.send(record.bytes);
1411
+ }
1412
+ catch (error) {
1413
+ flow.cancel(id);
1414
+ const cut = s.motionCut;
1415
+ if (error instanceof DesktopMediaAdmissionCancelled && current() && s.media === media && cut?.fence && error.cutId === cut.payload.cutId && error.epoch === packet.epoch && error.pictureSequence === packet.sequence) {
1416
+ this.disposeMotion(s, backend, packet, cut);
1417
+ active.delete(lane);
1418
+ updateAge();
1419
+ continue;
1420
+ }
1421
+ throw error;
1422
+ }
1423
+ flow.commit(id);
1424
+ if (!current() || s.configured !== configured || s.media !== media)
1425
+ return;
1426
+ s.lastPacketAt = performance.now();
1427
+ if (packet.type === 1) {
1428
+ s.configAdmittedEpoch = packet.epoch;
1429
+ this.wakeMedia(s);
1430
+ }
1431
+ if (packet.type === 2 || packet.type === 5) {
1432
+ s.activeBytes += packet.payload.length;
1433
+ if (packet.type === 2 && packet.fragmentIndex + 1 === packet.fragmentCount) {
1434
+ s.receivedFrame = Math.max(s.receivedFrame, packet.frameId);
1435
+ s.firstCompleteFrame ||= packet.frameId;
1436
+ if (!s.capturePaused)
1437
+ s.delivery.sent(packet.frameId, performance.now());
1438
+ }
1439
+ }
1440
+ stage = 'native_forwarded';
1441
+ backend.mediaForwarded?.(packet);
1442
+ active.delete(lane);
1443
+ updateAge();
1444
+ this.returnNativeCredit(s);
1445
+ this.wakeMedia(s);
1446
+ }
1447
+ }
1448
+ catch (error) {
1449
+ await fail(error, packet, stage);
1450
+ }
1451
+ };
1452
+ const workers = ['motion', 'cursor', 'exact'].map(worker);
1453
+ try {
1454
+ for await (const bytes of backend.packets) {
1455
+ if (!current() || failed)
1456
+ break;
1457
+ const p = decodeMedia(bytes);
1458
+ mediaRequire(s.configured && p.epoch === s.configured.mediaEpoch && p.displayRevision === s.configured.displayRevision && s.startedEpoch === p.epoch);
1459
+ if (p.type === 1)
1460
+ s.codec = decodeConfig(p.payload);
1461
+ mediaRequire(queuedBytes + s.flow.inFlightBytes + p.payload.length <= 524288 && queuedCount() + s.flow.recordCount < 128);
1462
+ const lane = desktopMediaLane(p.type);
1463
+ queues[lane].push({ bytes, p, at: performance.now() });
1464
+ queuedBytes += p.payload.length;
1465
+ updateAge();
1466
+ this.wakeMedia(s);
1467
+ }
1468
+ }
1469
+ catch (error) {
1470
+ await fail(error);
1471
+ }
1472
+ finally {
1473
+ ended = true;
1474
+ this.wakeMedia(s);
1475
+ await Promise.all(workers);
1476
+ if (current())
1477
+ s.queuedAt = 0;
1478
+ }
1479
+ }
1480
+ async rateTick(s) {
1481
+ if (s.tickBusy)
1482
+ return;
1483
+ s.tickBusy = true;
1484
+ try {
1485
+ await this.rateTickOwned(s);
1486
+ }
1487
+ finally {
1488
+ s.tickBusy = false;
1489
+ }
1490
+ }
1491
+ async rateTickOwned(s) {
1492
+ if (!this.current(s) || !s.backend || !s.configured || s.startedEpoch !== s.configured.mediaEpoch || s.starting || s.rateBusy || s.motionCut && s.motionCut.phase !== 'committed')
1493
+ return;
1494
+ const backend = s.backend, configured = s.configured;
1495
+ const transportCost = s.media?.transportCost?.();
1496
+ if (transportCost && backend.accountTransportCost)
1497
+ await backend.accountTransportCost(transportCost);
1498
+ const audioCost = s.control?.audioTransportCost?.();
1499
+ if (audioCost && backend.accountTransportCost)
1500
+ await backend.accountTransportCost(audioCost);
1501
+ if (!this.current(s) || s.backend !== backend || s.configured !== configured)
1502
+ return;
1503
+ if (backend.checkHealth && performance.now() >= (s.nextHealthAt ?? 0)) {
1504
+ s.nextHealthAt = performance.now() + 1000;
1505
+ try {
1506
+ await backend.checkHealth();
1507
+ }
1508
+ catch (error) {
1509
+ if (!this.current(s) || s.backend !== backend || s.configured !== configured)
1510
+ return;
1511
+ this.diagnostic(s, 'native_health', error);
1512
+ throw error;
1513
+ }
1514
+ const audioConfig = s.audioConfig;
1515
+ if (audioConfig && backend.audioStatus) {
1516
+ const status = await backend.audioStatus().catch(() => 'unavailable');
1517
+ if (status === 'unavailable' && this.current(s) && s.backend === backend && s.audioConfig === audioConfig) {
1518
+ s.audioConfig = undefined;
1519
+ s.control?.stopAudio?.();
1520
+ await backend.stopAudio?.().catch(() => { });
1521
+ await this.send(s, { v: 1, type: 'audio_state', sessionId: s.binding.sessionId,
1522
+ payload: { commandId: audioConfig.commandId, state: 'unavailable', reason: 'audio_capture_unavailable' } }).catch(() => { });
1523
+ }
1524
+ }
1525
+ }
1526
+ if (!this.current(s) || s.backend !== backend || s.configured !== configured || s.startedEpoch !== configured.mediaEpoch)
1527
+ return;
1528
+ const now = performance.now(), queued = s.queuedAt ? now - s.queuedAt : 0;
1529
+ if (now >= s.nextPing) {
1530
+ s.nextPing = now + 10000;
1531
+ for (const [challenge, value] of s.challenges)
1532
+ if (now - value.at >= 90000)
1533
+ s.challenges.delete(challenge);
1534
+ mediaRequire(s.challenges.size < 10);
1535
+ const challenge = randomBytes(16).toString('hex');
1536
+ s.challenges.set(challenge, { at: now, answered: false });
1537
+ await this.send(s, { v: 1, type: 'ping', sessionId: s.binding.sessionId, payload: { challenge } });
1538
+ if (!this.current(s) || s.backend !== backend || s.configured !== configured || s.startedEpoch !== configured.mediaEpoch)
1539
+ return;
1540
+ }
1541
+ // Credit starvation is bounded backpressure. Feed queue age into rate/admission
1542
+ // control instead of restarting a healthy helper when the remote reader is slow.
1543
+ const baseline = s.rtts.length ? Math.min(...s.rtts) : 0;
1544
+ const applied = backend.rateState?.applied ??
1545
+ { targetBps: configured.rateTargetBps, fpsCeiling: configured.fpsCeiling };
1546
+ const delivery = s.delivery.sample(now), excessRttMs = Math.max(0, s.rtt - baseline);
1547
+ const queueAgeMs = s.feedback?.queueAgeMs ?? 0;
1548
+ const timing = backend.nativeTelemetry?.lastFrameTiming;
1549
+ if (timing && !timing.repair && timing.epoch === configured.mediaEpoch) {
1550
+ const key = `${timing.epoch}:${timing.frameId}`;
1551
+ if (key !== s.motionEncodeKey) {
1552
+ s.motionEncodeKey = key;
1553
+ s.motionEncodeObservedAt = now;
1554
+ const elapsedUs = timing.encodeEndUs - timing.encodeBeginUs;
1555
+ s.motionEncodeMs = Number.isFinite(elapsedUs) && elapsedUs >= 0 ? elapsedUs / 1000 : backend.nativeTelemetry?.lastEncodeMs ?? 0;
1556
+ }
1557
+ }
1558
+ const encodeMs = s.motionEncodeMs;
1559
+ const decodeMs = s.feedback?.decodeMs ?? 0;
1560
+ const network = s.media?.networkSample?.();
1561
+ const decision = s.controller.sample({ nowMs: now, activeBytes: s.activeBytes,
1562
+ rttBaselineMs: baseline, excessRttMs, deliveryDelayMs: delivery.delayMs,
1563
+ deliveryObservedAtMs: s.feedbackProgressAt ?? 0, encodeObservedAtMs: s.motionEncodeObservedAt ?? 0,
1564
+ decodeObservedAtMs: s.decodeObservedAt ?? 0, network,
1565
+ queueAgeMs, queuedSendMs: queued, encodeMs, decodeMs }, backend.runtimeRateReconfigure === true);
1566
+ if (now - s.lastPerformanceDiagnosticAt >= (s.performanceDiagnosticCount < 30 ? 2000 : 10000)) {
1567
+ s.lastPerformanceDiagnosticAt = now;
1568
+ s.performanceDiagnosticCount = Math.min(30, s.performanceDiagnosticCount + 1);
1569
+ try {
1570
+ const native = backend.nativeTelemetry, previous = s.performanceSample;
1571
+ const current = native ? { atMs: now, epoch: configured.mediaEpoch, captured: native.captured, encoded: native.encoded,
1572
+ refinements: native.refinementFrames ?? 0, payloadBytes: native.sentPayloadBytes } : undefined;
1573
+ const comparable = current && previous && current.epoch === previous.epoch && now > previous.atMs &&
1574
+ current.encoded >= previous.encoded && current.refinements >= previous.refinements && current.payloadBytes >= previous.payloadBytes;
1575
+ const seconds = comparable ? (now - previous.atMs) / 1000 : 0;
1576
+ console.error(JSON.stringify({ event: 'desktop_supervisor_performance', session: s.diagnosticId,
1577
+ connectionId: s.diagnosticKey, processId: process.pid, atUnixMs: Date.now(),
1578
+ atMs: Math.round(now), epoch: configured.mediaEpoch, width: configured.width, height: configured.height,
1579
+ appliedTargetBps: applied.targetBps, appliedFpsCeiling: applied.fpsCeiling,
1580
+ queuedMs: Math.round(queued), queueAgeMs, excessRttMs, encodeMs, decodeMs,
1581
+ encodeMetric: 'source_unit_job', decodeMetric: 'submit_to_callback', networkScope: 'sender_to_server', network,
1582
+ presentMs: s.feedback?.presentMs, receiverQueueDepth: s.feedback?.queueDepth,
1583
+ policy: s.controller.diagnostics,
1584
+ captureConsumedFps: comparable && current.captured !== undefined && previous.captured !== undefined && current.captured >= previous.captured
1585
+ ? (current.captured - previous.captured) / seconds : undefined,
1586
+ motionCommittedFps: comparable ? (current.encoded - previous.encoded) / seconds : undefined,
1587
+ exactUpdatesPerSecond: comparable && native?.refinementFrames !== undefined ? (current.refinements - previous.refinements) / seconds : undefined,
1588
+ payloadBps: comparable ? (current.payloadBytes - previous.payloadBytes) * 8 / seconds : undefined,
1589
+ receivedFrame: s.receivedFrame, decodedId: s.feedback?.decodedId, presentedId: s.feedback?.presentedId,
1590
+ exactPresentedSequence: s.feedback?.exactPresentedSequence,
1591
+ native: backend.nativeTelemetry }));
1592
+ s.performanceSample = current;
1593
+ }
1594
+ catch { }
1595
+ }
1596
+ const directPressure = Math.max(excessRttMs, queueAgeMs, queued) > 40 ||
1597
+ Math.max(encodeMs, decodeMs) > 0.8 * 1000 / applied.fpsCeiling;
1598
+ // Independent optional lane; never await snapshot/IO in focus rate or input processing.
1599
+ const initialOverview = s.overviewSequence === 0;
1600
+ if (backend.overview && backend.reserveOverview && s.control?.sendOverview && !s.overviewBusy && now >= (s.overviewNextAt ?? 0) &&
1601
+ s.overviewBudget.canCapture(now, applied.targetBps, !initialOverview && (directPressure || s.capturePaused || s.rateBusy))) {
1602
+ s.overviewBusy = true;
1603
+ s.overviewNextAt = now + 2000;
1604
+ const sourceWidth = configured.captureRegion?.sourceWidth ?? configured.width;
1605
+ const sourceHeight = configured.captureRegion?.sourceHeight ?? configured.height;
1606
+ void (async () => {
1607
+ const image = await backend.overview(Math.min(s.overviewBudget.maxSide, s.overviewMaxSide ?? 640));
1608
+ if (!image) {
1609
+ // Poll only initial asynchronous capture setup promptly; do not add a
1610
+ // two-second scheduling delay after WindowServer has produced its image.
1611
+ if (this.current(s) && s.backend === backend)
1612
+ s.overviewNextAt = performance.now() +
1613
+ (s.overviewSequence === 0 && s.overviewInitialPolls++ < 20 ? 250 : 2000);
1614
+ return;
1615
+ }
1616
+ if (image.sourceWidth !== sourceWidth || image.sourceHeight !== sourceHeight || !this.current(s) || s.backend !== backend ||
1617
+ s.configured !== configured || !initialOverview && (s.queuedAt || s.capturePaused))
1618
+ return;
1619
+ const sequence = s.overviewSequence + 1;
1620
+ const bytes = overviewPacket(image, sequence, s.binding.controlGeneration);
1621
+ if (!s.overviewBudget.eligible(bytes, performance.now(), applied.targetBps))
1622
+ return;
1623
+ if (!await backend.reserveOverview({ epoch: configured.mediaEpoch, wireBytes: bytes.length })) {
1624
+ if (this.current(s) && s.backend === backend && s.configured === configured && initialOverview) {
1625
+ s.overviewNextAt = performance.now() + 250;
1626
+ s.overviewMaxSide = 320;
1627
+ }
1628
+ return;
1629
+ }
1630
+ if (!this.current(s) || s.backend !== backend || s.configured !== configured ||
1631
+ !initialOverview && (s.queuedAt || s.capturePaused))
1632
+ return;
1633
+ if (s.overviewBudget.admit(bytes, performance.now(), applied.targetBps)) {
1634
+ // Rejected captures have not used the initial preview's pressure exemption.
1635
+ s.overviewSequence = sequence;
1636
+ if (!await s.control.sendOverview(bytes))
1637
+ s.overviewBudget.disconnected();
1638
+ }
1639
+ })().catch(() => { }).finally(() => { s.overviewBusy = false; });
1640
+ }
1641
+ if (decision.pauseCapture !== s.capturePaused) {
1642
+ s.capturePaused = decision.pauseCapture;
1643
+ s.delivery.reset();
1644
+ }
1645
+ // Provider qualification, not the generic slow policy, decides whether a
1646
+ // target change needs to retire codec/input authority.
1647
+ if (backend.runtimeRateReconfigure)
1648
+ decision.reopenEpoch = decision.fpsCeiling >
1649
+ (backend.runtimeRateFpsCeiling ?? s.selection?.fpsCeiling ?? configured.fpsCeiling);
1650
+ if (decision.targetBps === applied.targetBps && decision.fpsCeiling === applied.fpsCeiling)
1651
+ decision.reopenEpoch = false;
1652
+ const rateKey = `${decision.targetBps}:${decision.fpsCeiling}:${decision.pauseCapture}:${decision.reopenEpoch}`;
1653
+ if (rateKey !== s.lastRateDiagnostic && now - s.lastRateDiagnosticAt >= 1000) {
1654
+ s.lastRateDiagnostic = rateKey;
1655
+ s.lastRateDiagnosticAt = now;
1656
+ s.rateDiagnosticCount++;
1657
+ try {
1658
+ console.error(JSON.stringify({ event: 'desktop_supervisor_rate_decision', session: s.diagnosticId,
1659
+ connectionId: s.diagnosticKey, processId: process.pid, atUnixMs: Date.now(), policy: s.controller.diagnostics,
1660
+ ordinal: s.rateDiagnosticCount, epoch: configured.mediaEpoch, targetBps: decision.targetBps,
1661
+ fpsCeiling: decision.fpsCeiling, pauseCapture: decision.pauseCapture, reopenEpoch: decision.reopenEpoch,
1662
+ appliedTargetBps: applied.targetBps, appliedFpsCeiling: applied.fpsCeiling,
1663
+ queuedMs: Math.round(queued), queueAgeMs: s.feedback?.queueAgeMs ?? 0, excessRttMs: Math.max(0, s.rtt - baseline),
1664
+ deliveryDelayMs: delivery.delayMs, rawDeliveryExcessMs: delivery.excessMs,
1665
+ deliveryObservations: delivery.observations, deliveryEvidenceKnown: delivery.delayMs !== undefined,
1666
+ presented: s.presented }));
1667
+ }
1668
+ catch { /* Fixed, capped diagnostics do not affect rate decisions. */ }
1669
+ }
1670
+ s.activeBytes = 0;
1671
+ if (!s.presented && decision.reopenEpoch)
1672
+ return;
1673
+ s.rateBusy = true;
1674
+ try {
1675
+ if (decision.reopenEpoch) {
1676
+ if (!this.current(s) || s.backend !== backend || s.configured !== configured || s.startedEpoch !== configured.mediaEpoch)
1677
+ return;
1678
+ const commandId = randomBytes(16).toString('hex');
1679
+ s.configuredCommand = commandId;
1680
+ s.automaticConfiguration = commandId;
1681
+ s.backend?.configurationCommand?.(commandId);
1682
+ }
1683
+ if (this.current(s) && s.backend === backend && s.configured === configured && (decision.reopenEpoch || s.startedEpoch === configured.mediaEpoch))
1684
+ await backend.setRate(decision);
1685
+ }
1686
+ finally {
1687
+ s.rateBusy = false;
1688
+ if (s.recoveryPending) {
1689
+ s.recoveryPending = false;
1690
+ await this.recover(s);
1691
+ }
1692
+ }
1693
+ }
1694
+ async recover(s) {
1695
+ if (!this.current(s))
1696
+ return;
1697
+ // A failed media lane cannot be forgotten merely because the encoder is
1698
+ // settling a rate command. Coalesce it and recover as soon as that owner exits.
1699
+ if (s.rateBusy) {
1700
+ if (!s.recovering)
1701
+ s.recoveryPending = true;
1702
+ return;
1703
+ }
1704
+ const now = performance.now();
1705
+ s.recovery = s.recovery.filter(at => now - at < 30000);
1706
+ if (s.recovery.length >= 2 || !s.selection) {
1707
+ await this.fail(s);
1708
+ return;
1709
+ }
1710
+ s.recovery.push(now);
1711
+ s.rateBusy = true;
1712
+ s.recovering = true;
1713
+ try {
1714
+ const backend = s.backend;
1715
+ s.backend = undefined;
1716
+ s.audioConfig = undefined;
1717
+ s.audioFeedback = undefined;
1718
+ s.audioReady = false;
1719
+ s.control?.stopAudio?.();
1720
+ if (s.audioCommand && s.audioMode && s.audioMode !== 'off')
1721
+ await this.send(s, { v: 1, type: 'audio_state',
1722
+ sessionId: s.binding.sessionId, payload: { commandId: s.audioCommand, state: 'suspended', reason: 'audio_source_restarting' } });
1723
+ s.mediaAttaching = undefined;
1724
+ s.media?.close();
1725
+ s.media = undefined;
1726
+ s.packetLoop = false;
1727
+ s.viewerReady = false;
1728
+ s.decoderReady = false;
1729
+ s.presented = false;
1730
+ s.startedEpoch = 0;
1731
+ s.starting = false;
1732
+ s.startCommand = undefined;
1733
+ await backend?.stop();
1734
+ if (!this.current(s))
1735
+ return;
1736
+ const commandId = randomBytes(16).toString('hex');
1737
+ s.configuredCommand = commandId;
1738
+ await this.send(s, { v: 1, type: 'media_open', sessionId: s.binding.sessionId, payload: { commandId } });
1739
+ if (!this.current(s))
1740
+ return;
1741
+ s.preparation = this.options.provider.prepare({ ...s.selection, commandId }, s.binding);
1742
+ const next = await s.preparation;
1743
+ if (!this.current(s)) {
1744
+ await next.stop();
1745
+ return;
1746
+ }
1747
+ s.backend = next;
1748
+ void this.events(s, next);
1749
+ }
1750
+ catch (error) {
1751
+ if (this.current(s))
1752
+ this.diagnostic(s, 'recovery_failed', error);
1753
+ await this.fail(s);
1754
+ }
1755
+ finally {
1756
+ s.rateBusy = false;
1757
+ }
1758
+ }
1759
+ async close() {
1760
+ this.clearLobbies();
1761
+ const candidates = [...this.candidates.values()];
1762
+ for (const candidate of candidates)
1763
+ this.retire(candidate);
1764
+ const active = this.active;
1765
+ if (active)
1766
+ this.retire(active);
1767
+ this.closed = true;
1768
+ await this.maintenance?.catch(() => { });
1769
+ await this.tail;
1770
+ if (active)
1771
+ await this.cleanup(active);
1772
+ await Promise.all(candidates.map(candidate => this.cleanup(candidate)));
1773
+ }
1774
+ }