@platless/axom-cli 0.5.9 → 0.6.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +18 -5
  2. package/dist/axom-patch/cli/README.md +100 -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/codex-account-limits.json +11 -11
  7. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  8. package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
  9. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  10. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  11. package/dist/axom-patch/cli/nightly-task.md +55 -13
  12. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  13. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  14. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1591 -0
  15. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +360 -0
  16. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
  17. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
  18. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  19. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  20. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  21. package/dist/axom-patch/cli/runtime.json +16 -18
  22. package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
  23. package/dist/axom-patch/cli/zcode-models.json +1 -2
  24. package/dist/axom-patch/cli/zcode-subagent-subwork.json +9 -10
  25. package/dist/cli.js +4 -36
  26. package/dist/commands/knowledge.js +47 -0
  27. package/dist/commands/update.js +65 -3
  28. package/dist/commands/workstation.js +73 -7
  29. package/dist/install/claude/commands.js +13 -13
  30. package/dist/mcp.js +1 -1
  31. package/dist/native/darwin-arm64/axom-quic +0 -0
  32. package/dist/workstation/acp/acpAgents.js +102 -98
  33. package/dist/workstation/acp/acpClient.js +26 -0
  34. package/dist/workstation/acp/builds/install/npm.js +99 -10
  35. package/dist/workstation/acp/initializeResponse.js +6 -1
  36. package/dist/workstation/acp/managedPatches.js +189 -25
  37. package/dist/workstation/acp/managedRuntime.js +314 -0
  38. package/dist/workstation/acp/patchAssets.js +40 -0
  39. package/dist/workstation/acp/processor/checkpoint.js +25 -34
  40. package/dist/workstation/acp/processor/control.js +5 -78
  41. package/dist/workstation/acp/processor/runtime.js +5 -81
  42. package/dist/workstation/acp/processor/session.js +51 -80
  43. package/dist/workstation/acp/processor/state.js +6 -13
  44. package/dist/workstation/acp/processor/step.js +3 -20
  45. package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
  46. package/dist/workstation/acp/processor/updates.js +131 -105
  47. package/dist/workstation/acp/processorSession.js +8 -172
  48. package/dist/workstation/acp/registry/catalog.js +65 -39
  49. package/dist/workstation/acp/registry/launch.js +16 -1
  50. package/dist/workstation/acp/switching.js +0 -3
  51. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  52. package/dist/workstation/acp/test/keeperWorld.js +28 -0
  53. package/dist/workstation/acp/test/raw/sources.js +16 -130
  54. package/dist/workstation/acp/test/realAdapter.js +1 -47
  55. package/dist/workstation/acp/test/scriptAgent.js +11 -3
  56. package/dist/workstation/agentFlow.js +1 -1
  57. package/dist/workstation/agentSessions.js +38 -143
  58. package/dist/workstation/channels/management.js +95 -11
  59. package/dist/workstation/channels/url.js +1 -1
  60. package/dist/workstation/config.js +16 -4
  61. package/dist/workstation/connection/encryptedChannel.js +4 -1
  62. package/dist/workstation/control/bindingStore.js +200 -0
  63. package/dist/workstation/control/commands.js +124 -0
  64. package/dist/workstation/control/daemonIdentity.js +53 -0
  65. package/dist/workstation/control/enrollment.js +106 -0
  66. package/dist/workstation/control/inspect.js +71 -0
  67. package/dist/workstation/control/locks.js +63 -0
  68. package/dist/workstation/control/protocol.js +40 -0
  69. package/dist/workstation/control/restartGuard.js +46 -0
  70. package/dist/workstation/control/runtimeClient.js +97 -0
  71. package/dist/workstation/control/runtimeHost.js +148 -0
  72. package/dist/workstation/control/serverIdentity.js +35 -0
  73. package/dist/workstation/daemon.js +106 -6
  74. package/dist/workstation/daemonctl.js +77 -14
  75. package/dist/workstation/desktopControl/audio.js +99 -0
  76. package/dist/workstation/desktopControl/capabilities.js +92 -0
  77. package/dist/workstation/desktopControl/captureModes.js +9 -0
  78. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  79. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  80. package/dist/workstation/desktopControl/helperProcess.js +353 -0
  81. package/dist/workstation/desktopControl/lobby.js +129 -0
  82. package/dist/workstation/desktopControl/macProvider.js +155 -0
  83. package/dist/workstation/desktopControl/media.js +331 -0
  84. package/dist/workstation/desktopControl/nativeProvider.js +1018 -0
  85. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  86. package/dist/workstation/desktopControl/overview.js +58 -0
  87. package/dist/workstation/desktopControl/protocol.js +302 -0
  88. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  89. package/dist/workstation/desktopControl/rateController.js +226 -0
  90. package/dist/workstation/desktopControl/supervisor.js +1681 -0
  91. package/dist/workstation/desktopControl/transport.js +27 -0
  92. package/dist/workstation/desktopControl/wallpaper.js +78 -0
  93. package/dist/workstation/files/credentials.js +142 -0
  94. package/dist/workstation/files/text.js +42 -8
  95. package/dist/workstation/keepers/kit/bundle.js +21 -17
  96. package/dist/workstation/keepers/kit/launcher.js +47 -7
  97. package/dist/workstation/keepers/kit/paths.js +2 -1
  98. package/dist/workstation/managedInstall.js +57 -0
  99. package/dist/workstation/protocol/agent.js +2 -15
  100. package/dist/workstation/protocol/management.js +93 -4
  101. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  102. package/dist/workstation/rd/artifact.js +119 -0
  103. package/dist/workstation/rd/control.js +119 -0
  104. package/dist/workstation/rd/display.js +29 -0
  105. package/dist/workstation/rd/provider.js +222 -0
  106. package/dist/workstation/rd/sessions.js +391 -0
  107. package/dist/workstation/rd/state.js +45 -0
  108. package/dist/workstation/rd/update.js +171 -0
  109. package/dist/workstation/rd/upkeep.js +38 -0
  110. package/dist/workstation/register.js +27 -37
  111. package/dist/workstation/service/installer.js +104 -13
  112. package/dist/workstation/service/launchd.js +1 -0
  113. package/dist/workstation/storageMetrics.js +118 -0
  114. package/dist/workstation/systemMetrics.js +44 -92
  115. package/dist/workstation/toolchain.js +94 -0
  116. package/dist/workstation/tools/browser.js +28 -4
  117. package/dist/workstation/update/artifact.js +2 -1
  118. package/dist/workstation/update/globalInstall.js +116 -0
  119. package/dist/workstation/update/npmInstaller.js +49 -23
  120. package/dist/workstation/update/rdRollback.js +145 -0
  121. package/dist/workstation/update/runUpdate.js +3 -2
  122. package/dist/workstation/update/watchdog.js +16790 -334
  123. package/package.json +5 -5
  124. package/skills/axom/SKILL.md +11 -11
  125. package/skills/axom-definition/SKILL.md +3 -3
  126. package/skills/axom-platform/SKILL.md +3 -3
  127. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  128. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
  129. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  130. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  131. package/dist/commands/guide.js +0 -38
  132. package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
  133. package/dist/runtime-deps/qoder-sdk/package.json +0 -8
  134. package/dist/workstation/acp/delegationMcp.js +0 -24
  135. package/dist/workstation/acp/processor/sideChannel.js +0 -80
  136. package/dist/workstation/acp/qoderStartup.js +0 -45
  137. package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
  138. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
  139. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
  140. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
  141. package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
  142. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  143. package/dist/workstation/qoderAdapter/contract.js +0 -78
  144. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
  145. package/dist/workstation/qoderAdapter/history.js +0 -85
  146. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  147. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  148. package/dist/workstation/qoderAdapter/main.js +0 -95
  149. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  150. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  151. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  152. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  153. package/dist/workstation/qoderAdapter/profile.js +0 -93
  154. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
  155. package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
  156. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
  157. package/dist/workstation/qoderAdapter/session.js +0 -319
  158. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  159. package/dist/workstation/qoderAdapter/subwork.js +0 -85
  160. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  161. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  162. package/dist/workstation/qoderAdapter/usage.js +0 -37
  163. /package/dist/workstation/{qoderAdapter/types.js → desktopControl/provider.js} +0 -0
@@ -0,0 +1,1018 @@
1
+ import { promises as fs, constants } from 'node:fs';
2
+ import { execFile } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ import path from 'node:path';
5
+ import { createHash } from 'node:crypto';
6
+ import { setTimeout as delay } from 'node:timers/promises';
7
+ import { z } from 'zod';
8
+ import { HelperProcess, DesktopQueue, NativeCommandError, bounded } from './helperProcess.js';
9
+ import { DesktopMediaStream, decodeMedia, desktopMediaLane, mediaRequire } from './media.js';
10
+ import { canonicalDesktopJson, decodeCapabilityReport, decodeDesktopControl, supportsDesktopMode, captureRegion } from './protocol.js';
11
+ import { captureSamplingModes } from './captureModes.js';
12
+ import { captureTargetLabel } from './captureTargetLabel.js';
13
+ import { XfceWallpaperReader, decodeWallpaper } from './wallpaper.js';
14
+ const execute = promisify(execFile);
15
+ const totals = ['captured', 'encoded', 'videoPayloadBytes', 'headerBytes', 'rateReopens'];
16
+ class ViewerLifetime {
17
+ epoch = 0;
18
+ nextFrameId = 1;
19
+ controlGeneration = 0;
20
+ deviceGeneration = '';
21
+ inputSequence = 0;
22
+ lastAppliedInputSequence = 0;
23
+ text = new Map();
24
+ pendingText;
25
+ initialReservoirBits = 1_000_000;
26
+ appliedTargetBps = 4_000_000;
27
+ appliedFps = 30;
28
+ prepared = false;
29
+ transportDebtBits = 0;
30
+ transportCosts = new Map();
31
+ offsets = { captured: 0, encoded: 0, videoPayloadBytes: 0, headerBytes: 0, rateReopens: 0 };
32
+ session;
33
+ touched = performance.now();
34
+ reserve() {
35
+ mediaRequire(this.epoch < 0xffffffff && Number.isSafeInteger(this.nextFrameId + 1_000_000));
36
+ const nextFrameId = this.nextFrameId;
37
+ this.nextFrameId += 1_000_000;
38
+ return { epoch: ++this.epoch, nextFrameId, frameIdLimit: this.nextFrameId - 1 };
39
+ }
40
+ bind(binding) {
41
+ if (this.controlGeneration !== binding.controlGeneration || this.deviceGeneration !== binding.deviceGeneration) {
42
+ this.controlGeneration = binding.controlGeneration;
43
+ this.deviceGeneration = binding.deviceGeneration;
44
+ this.inputSequence = 0;
45
+ this.lastAppliedInputSequence = 0;
46
+ this.text.clear();
47
+ this.pendingText = undefined;
48
+ }
49
+ }
50
+ }
51
+ /** No helper/session construction in discovery. Only the image's admitted software is offered. */
52
+ export class NativeProvider {
53
+ options;
54
+ lifetimes = new Map();
55
+ revision = 1;
56
+ geometry;
57
+ geometryTail = Promise.resolve();
58
+ geometryJobs = 0;
59
+ displayId;
60
+ factory;
61
+ windowRevisions = new Map();
62
+ executable;
63
+ constructor(options) {
64
+ this.options = options;
65
+ mediaRequire(path.isAbsolute(options.helperPath) && path.isAbsolute(options.runtimeDir));
66
+ this.displayId = '00000000000000000000000000000001';
67
+ this.executable = options.helperPath;
68
+ this.factory = options.helperFactory ?? (binding => HelperProcess.launch(this.executable, { ...this.environment,
69
+ ...(binding ? { AXOM_KEEPER_TARGET: binding.displayId } : {}) }));
70
+ }
71
+ get environment() {
72
+ return this.options.environment ?? { ...process.env, XDG_RUNTIME_DIR: this.options.runtimeDir };
73
+ }
74
+ get displayControl() {
75
+ const executable = this.environment.AXOM_DESKTOP_DISPLAY_CONTROL ?? '/usr/local/bin/axom-display';
76
+ mediaRequire(path.isAbsolute(executable));
77
+ return executable;
78
+ }
79
+ async readBounded(file, limit = 49152) {
80
+ const handle = await fs.open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
81
+ try {
82
+ const info = await handle.stat();
83
+ mediaRequire(info.isFile() && info.size <= limit);
84
+ const bytes = Buffer.alloc(limit + 1);
85
+ const read = await handle.read(bytes, 0, limit + 1, 0);
86
+ mediaRequire(read.bytesRead <= limit);
87
+ return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(0, read.bytesRead)));
88
+ }
89
+ finally {
90
+ await handle.close();
91
+ }
92
+ }
93
+ withGeometry(run) {
94
+ mediaRequire(this.geometryJobs < 16);
95
+ this.geometryJobs++;
96
+ const next = this.geometryTail.then(run);
97
+ this.geometryTail = next.catch(() => { }).finally(() => { this.geometryJobs--; });
98
+ return next;
99
+ }
100
+ probe() { return this.withGeometry(() => this.probeCurrent()); }
101
+ async wallpaper(targetId) {
102
+ mediaRequire(/^[a-f0-9]{32}$/.test(targetId));
103
+ const capabilities = await this.probe();
104
+ mediaRequire(capabilities?.displays.some(display => display.id === targetId));
105
+ const reader = new XfceWallpaperReader(async (command, args) => {
106
+ const result = await execute(command, args, { timeout: 1500, maxBuffer: 49152,
107
+ env: this.environment });
108
+ return result.stdout;
109
+ });
110
+ const discovered = await reader.discover();
111
+ const result = await execute(this.executable, ['--wallpaper-asset', discovered.file, discovered.background], { timeout: 4000, maxBuffer: 100000, env: this.environment });
112
+ const wallpaper = decodeWallpaper(result.stdout);
113
+ return { asset: { ...wallpaper.asset, placement: discovered.placement,
114
+ background: discovered.background, fidelity: discovered.fidelity === 'approximate' ? 'approximate' : wallpaper.asset.fidelity }, jpeg: wallpaper.jpeg };
115
+ }
116
+ async probeCurrent() {
117
+ if (this.options.enumerate)
118
+ return this.options.enumerate();
119
+ const c = decodeCapabilityReport(JSON.stringify({ generation: 'local', revision: 1,
120
+ desktop: await this.readBounded(this.options.manifestPath ?? '/opt/axom-desktop/provider.json') })).desktop;
121
+ mediaRequire(c && c.encoderBackends.length === 1 && c.encoderBackends[0].id === 'libx264' && c.encoderBackends[0].kind === 'software');
122
+ await fs.access(this.executable, constants.X_OK);
123
+ const identity = z.strictObject({ continuationAbi: z.literal(1), helperSha256: z.string().regex(/^[a-f0-9]{64}$/) }).parse(await this.readBounded(path.join(path.dirname(this.options.manifestPath ?? '/opt/axom-desktop/provider.json'), 'provider-identity.json'), 1024));
124
+ const binary = await fs.open(this.executable, constants.O_RDONLY | constants.O_NOFOLLOW);
125
+ try {
126
+ const info = await binary.stat();
127
+ mediaRequire(info.isFile() && info.size <= 32 * 1024 * 1024);
128
+ const hash = createHash('sha256');
129
+ for await (const bytes of binary.createReadStream({ autoClose: false }))
130
+ hash.update(bytes);
131
+ mediaRequire(hash.digest('hex') === identity.helperSha256);
132
+ }
133
+ finally {
134
+ await binary.close();
135
+ }
136
+ await fs.access(this.environment.XAUTHORITY ?? '/run/axom-desktop/Xauthority', constants.R_OK);
137
+ const runtime = await fs.stat(this.options.runtimeDir);
138
+ mediaRequire(runtime.isDirectory() && runtime.uid === process.getuid?.() && !(runtime.mode & 0o022));
139
+ const geometry = z.object({ width: z.number().int().min(2).max(4096), height: z.number().int().min(2).max(4096),
140
+ dpi: z.number().int().min(64).max(384).optional() }).parse(await this.readBounded(path.join(this.options.runtimeDir, 'geometry.json'), 1024));
141
+ if (c.displays[0].maxDpi !== undefined)
142
+ mediaRequire(geometry.dpi !== undefined && geometry.dpi <= c.displays[0].maxDpi);
143
+ if (this.geometry && (geometry.width !== this.geometry.width || geometry.height !== this.geometry.height || geometry.dpi !== this.geometry.dpi))
144
+ this.nextRevision();
145
+ this.geometry = geometry;
146
+ let failed = false;
147
+ for (const name of ['display-failed', 'input-failed']) {
148
+ try {
149
+ await fs.lstat(path.join(this.options.runtimeDir, name));
150
+ failed = true;
151
+ }
152
+ catch (error) {
153
+ if (error.code !== 'ENOENT')
154
+ throw error;
155
+ }
156
+ }
157
+ const root = c.displays[0];
158
+ const sampling = root.captureViewport === true ? { capturePixelScale: 1, resize: 'dynamic',
159
+ resizeRange: { minWidth: 2, minHeight: 2, maxWidth: geometry.width, maxHeight: geometry.height },
160
+ modes: captureSamplingModes(geometry.width, geometry.height) } : {};
161
+ const displays = [{ ...root, id: this.displayId, ...geometry, ...sampling, revision: this.revision }];
162
+ if (c.providerVersion === 'axom-linux-2' && !failed) {
163
+ const { stdout } = await execute(this.executable, ['--capture-targets'], { env: this.environment, timeout: 2500, maxBuffer: 49152 });
164
+ const axis = z.number().int().min(2).max(4096);
165
+ const targets = z.array(z.strictObject({ id: z.string().regex(/^[0-9a-f]{32}$/), kind: z.literal('window'), label: z.string().max(4096), width: axis, height: axis })).max(127).parse(JSON.parse(stdout));
166
+ for (const target of targets) {
167
+ const old = this.windowRevisions.get(target.id);
168
+ const revision = old ? old.revision + Number(old.width !== target.width || old.height !== target.height) : 1;
169
+ this.windowRevisions.set(target.id, { width: target.width, height: target.height, revision });
170
+ displays.push({ ...target, label: captureTargetLabel(target.label), revision, resize: 'dynamic', captureViewport: true, capturePixelScale: 1, ...(root.overview ? { overview: true } : {}),
171
+ resizeRange: { minWidth: 2, minHeight: 2, maxWidth: target.width & ~1, maxHeight: target.height & ~1 }, modes: captureSamplingModes(target.width, target.height) });
172
+ }
173
+ const live = new Set(targets.map(t => t.id));
174
+ for (const key of this.windowRevisions.keys())
175
+ if (!live.has(key))
176
+ this.windowRevisions.delete(key);
177
+ }
178
+ return { ...c, cursorEmbedded: false, health: failed ? 'unavailable' : 'ready', reason: failed ? 'recovery_unverified' : null, displays };
179
+ }
180
+ nextRevision() { mediaRequire(this.revision < 0xffffffff); return ++this.revision; }
181
+ /** Publish dimensions and revision only after the native transaction verifies them. */
182
+ geometryTransaction(run, sampling = false) {
183
+ return this.withGeometry(async () => {
184
+ mediaRequire(this.revision < 0xffffffff);
185
+ const revision = this.revision + 1, actual = await run(revision);
186
+ mediaRequire(Number.isSafeInteger(actual.displayRevision) && actual.displayRevision >= revision && actual.displayRevision <= 0xffffffff);
187
+ if (!sampling)
188
+ this.geometry = { width: actual.width, height: actual.height, ...(actual.dpi === undefined ? {} : { dpi: actual.dpi }) };
189
+ else if (this.geometry && actual.dpi !== undefined)
190
+ this.geometry = { ...this.geometry, dpi: actual.dpi };
191
+ this.revision = actual.displayRevision;
192
+ return actual;
193
+ });
194
+ }
195
+ async prepare(selection, binding) {
196
+ await this.options.validateSession?.();
197
+ const caps = await this.probe();
198
+ mediaRequire(caps?.health === 'ready' && ['granted', 'not_applicable'].includes(caps.permission));
199
+ if (selection.cursorVisible !== undefined)
200
+ mediaRequire(caps.cursorVisibility === true);
201
+ const display = caps.displays.find(d => d.id === binding.displayId);
202
+ mediaRequire(display);
203
+ // Capture sampling keeps the physical source geometry independent of the
204
+ // encoded output size. Physical-resolution sessions use their configured grid.
205
+ const effectiveRegion = selection.captureRegion ?? (display.captureViewport === true ? {
206
+ x: 0, y: 0, width: display.width, height: display.height,
207
+ sourceWidth: display.width, sourceHeight: display.height,
208
+ } : undefined);
209
+ if (selection.captureRegion !== undefined) {
210
+ const region = captureRegion.parse(selection.captureRegion);
211
+ mediaRequire(display.captureViewport === true && region.sourceWidth === display.width && region.sourceHeight === display.height);
212
+ }
213
+ if (selection.dpi !== undefined)
214
+ mediaRequire(display.maxDpi !== undefined && selection.dpi <= display.maxDpi);
215
+ const format = caps.encoderBackends.find(b => b.id === (this.options.backend ?? 'libx264'))?.formats.find(f => f.format === selection.format && f.cappedRate);
216
+ mediaRequire(format && selection.width <= format.maxWidth && selection.height <= format.maxHeight &&
217
+ selection.width * selection.height <= format.maxPixels && selection.fpsCeiling <= format.maxFps &&
218
+ supportsDesktopMode(display, selection.width, selection.height));
219
+ for (const [id, life] of this.lifetimes)
220
+ if (!life.session && performance.now() - life.touched > 60000)
221
+ this.lifetimes.delete(id);
222
+ let life = this.lifetimes.get(binding.sessionId);
223
+ if (!life) {
224
+ mediaRequire(this.lifetimes.size < 1024);
225
+ life = new ViewerLifetime();
226
+ this.lifetimes.set(binding.sessionId, life);
227
+ }
228
+ if (life.session)
229
+ await life.session.stop();
230
+ life.bind(binding);
231
+ let revision = Math.max(this.revision, display.revision);
232
+ if ((display.captureViewport !== true && (selection.width !== display.width || selection.height !== display.height)) || selection.dpi !== undefined) {
233
+ const physicalWidth = display.captureViewport === true ? display.width : selection.width;
234
+ const physicalHeight = display.captureViewport === true ? display.height : selection.height;
235
+ const actual = await this.geometryTransaction(async (displayRevision) => {
236
+ const { stdout } = await execute(this.displayControl, ['--transaction', `${physicalWidth}x${physicalHeight}${selection.dpi === undefined ? '' : '@' + selection.dpi}`], { env: this.environment, timeout: 4500, maxBuffer: 4096 });
237
+ const result = z.object({ outcome: z.literal('applied'), width: z.literal(physicalWidth), height: z.literal(physicalHeight),
238
+ dpi: selection.dpi === undefined ? z.number().optional() : z.literal(selection.dpi) }).parse(JSON.parse(stdout));
239
+ return { ...result, displayRevision };
240
+ });
241
+ revision = actual.displayRevision;
242
+ }
243
+ const helper = await (this.options.sessionHelperFactory?.(binding) ?? this.factory(binding));
244
+ const identity = { sessionId: binding.sessionId, deviceGeneration: binding.deviceGeneration,
245
+ controlGeneration: binding.controlGeneration, displayRevision: revision };
246
+ const reserved = life.reserve();
247
+ try {
248
+ mediaRequire((await helper.request('verify_cleanup', identity)).result.verified === true);
249
+ const payload = { ...(selection.cursorVisible === undefined ? {} : { cursorVisible: selection.cursorVisible }), ...(effectiveRegion ? { captureRegion: effectiveRegion } : {}), width: selection.width, height: selection.height, fpsCeiling: life.prepared ? Math.min(life.appliedFps, selection.fpsCeiling) : selection.fpsCeiling,
250
+ rateTargetBps: life.appliedTargetBps, quality: selection.quality, backend: this.options.backend ?? 'libx264', leaseMs: 30000, representation: 1,
251
+ ...reserved, ...(life.prepared ? { continuation: { initialReservoirBits: life.initialReservoirBits, inputSequence: life.inputSequence,
252
+ lastAppliedInputSequence: life.lastAppliedInputSequence } } : {}) };
253
+ const reply = await helper.request('prepare', identity, payload);
254
+ const actual = z.strictObject({ backend: z.literal(this.options.backend ?? 'libx264'), width: z.literal(selection.width), height: z.literal(selection.height),
255
+ fpsCeiling: z.literal(payload.fpsCeiling), rateTargetBps: z.literal(payload.rateTargetBps), representation: z.literal(payload.representation) }).parse(reply.result.selection);
256
+ if (!life.prepared)
257
+ life.initialReservoirBits = actual.rateTargetBps / 4;
258
+ life.appliedFps = actual.fpsCeiling;
259
+ life.prepared = true;
260
+ const session = new NativeBackendSession(helper, life, identity, selection, reserved, this, effectiveRegion);
261
+ life.session = session;
262
+ return session;
263
+ }
264
+ catch (error) {
265
+ await helper.stop(identity);
266
+ throw error;
267
+ }
268
+ }
269
+ async recoverDisplay(binding, reserved) {
270
+ return this.geometryTransaction(async () => {
271
+ const helper = await (this.options.sessionHelperFactory?.(binding) ?? this.factory(binding));
272
+ const identity = { sessionId: binding.sessionId, deviceGeneration: binding.deviceGeneration, controlGeneration: binding.controlGeneration,
273
+ displayRevision: reserved.displayRevision };
274
+ try {
275
+ mediaRequire((await helper.request('verify_cleanup', identity)).result.verified === true);
276
+ const { result } = await helper.request('recover_display', identity);
277
+ const actual = z.object({ outcome: z.literal('recovered'), width: z.number().int().min(2).max(4096), height: z.number().int().min(2).max(4096) }).parse(result);
278
+ return { verified: true, width: actual.width, height: actual.height, displayRevision: reserved.displayRevision };
279
+ }
280
+ finally {
281
+ await helper.stop(identity);
282
+ }
283
+ });
284
+ }
285
+ }
286
+ export class NativeBackendSession {
287
+ helper;
288
+ life;
289
+ identity;
290
+ selection;
291
+ reserved;
292
+ provider;
293
+ effectiveRegion;
294
+ get audioSupported() { return this.helper.audioSupported === true; }
295
+ get audioPackets() { return this.helper.audioPackets; }
296
+ async startAudio(audioEpoch, bitrate) {
297
+ return this.serialized(async () => {
298
+ mediaRequire(!this.stopped && this.life.session === this && this.audioSupported);
299
+ const reply = await this.helper.request('audio_start', this.identity, { audioEpoch, bitrate });
300
+ return z.strictObject({ audioEpoch: z.literal(audioEpoch), sampleRate: z.literal(48000), channels: z.literal(2),
301
+ framesPerPacket: z.literal(960), bitrate: z.literal(bitrate), lookaheadFrames: z.number().int().min(0).max(960) }).parse(reply.result);
302
+ });
303
+ }
304
+ async setAudioRate(bitrate) {
305
+ return this.serialized(async () => {
306
+ mediaRequire(!this.stopped && this.life.session === this && this.audioSupported);
307
+ await this.helper.request('audio_rate', this.identity, { bitrate });
308
+ });
309
+ }
310
+ async stopAudio() {
311
+ return this.serialized(async () => {
312
+ if (!this.stopped && this.audioSupported)
313
+ await this.helper.request('audio_stop', this.identity);
314
+ });
315
+ }
316
+ async audioStatus() {
317
+ const reply = await this.helper.request('audio_status', this.identity);
318
+ return z.strictObject({ state: z.enum(['off', 'playing', 'unavailable']) }).parse(reply.result).state;
319
+ }
320
+ runtimeRate = false;
321
+ get runtimeRateReconfigure() { return this.runtimeRate; }
322
+ codecFpsCeiling;
323
+ get runtimeRateFpsCeiling() { return this.codecFpsCeiling; }
324
+ events = new DesktopQueue();
325
+ stopped = false;
326
+ stopPromise;
327
+ started = false;
328
+ captureEpoch = 0;
329
+ configuration;
330
+ stream = new DesktopMediaStream(() => this.effectiveRegion ? { width: this.effectiveRegion.sourceWidth, height: this.effectiveRegion.sourceHeight } : undefined);
331
+ desired;
332
+ applied;
333
+ reservedRate;
334
+ firstFrame = 0;
335
+ presented = false;
336
+ packetConsumer = false;
337
+ lastCounters;
338
+ transportTotal = 0;
339
+ transportRestored = false;
340
+ operationTail = Promise.resolve();
341
+ operationCount = 0;
342
+ boundaryAttempt;
343
+ retiringMedia;
344
+ pendingPackets = new Map();
345
+ pendingPacketBytes = 0;
346
+ readPayloadEpoch = 0;
347
+ readPayloadBytes = 0;
348
+ cutSettledFrame = 0;
349
+ motionCut;
350
+ packetKey(packet) { return `${packet.epoch}:${desktopMediaLane(packet.type)}:${packet.sequence}`; }
351
+ forwardingUnit = false;
352
+ forwardingCursor = false;
353
+ forwardedFrame;
354
+ drainWake;
355
+ constructor(helper, life, identity, selection, reserved, provider, effectiveRegion) {
356
+ this.helper = helper;
357
+ this.life = life;
358
+ this.identity = identity;
359
+ this.selection = selection;
360
+ this.reserved = reserved;
361
+ this.provider = provider;
362
+ this.effectiveRegion = effectiveRegion;
363
+ this.desired = this.applied = { targetBps: life.appliedTargetBps, fpsCeiling: life.prepared ? Math.min(life.appliedFps, selection.fpsCeiling) : selection.fpsCeiling, pauseCapture: false, reopenEpoch: false };
364
+ this.codecFpsCeiling = this.applied.fpsCeiling;
365
+ this.forwardedFrame = helper.counters?.lastAllocatedFrameId ?? 0;
366
+ this.publishConfigured();
367
+ void helper.closed.then(() => { if (!this.stopped)
368
+ this.events.push({ type: 'failed', code: 'encoder_failed' }); });
369
+ }
370
+ get nativeTelemetry() { return this.helper.counters; }
371
+ get epoch() { return this.reserved.epoch; }
372
+ get displayRevision() { return this.identity.displayRevision; }
373
+ focusRevision = 1;
374
+ async focus(focus) {
375
+ return this.serialized(async () => {
376
+ const region = this.effectiveRegion;
377
+ mediaRequire(this.presented && this.selection.captureRegion && region && focus.revision > this.focusRevision &&
378
+ focus.region.sourceWidth === region.sourceWidth && focus.region.sourceHeight === region.sourceHeight);
379
+ const result = await this.command('focus', { epoch: this.epoch, displayRevision: this.displayRevision, focus });
380
+ const checked = decodeDesktopControl(JSON.stringify({ v: 1, type: 'focus_result', sessionId: this.identity.sessionId,
381
+ payload: { commandId: this.selection.commandId, controlGeneration: this.identity.controlGeneration, mediaEpoch: this.epoch,
382
+ displayRevision: this.displayRevision, ...result } }), 'device');
383
+ mediaRequire(checked.type === 'focus_result');
384
+ if (checked.payload.outcome === 'applied') {
385
+ mediaRequire(canonicalDesktopJson(checked.payload.focus) === canonicalDesktopJson(focus));
386
+ this.focusRevision = focus.revision;
387
+ this.effectiveRegion = focus.region;
388
+ }
389
+ return { focus: checked.payload.focus, outcome: checked.payload.outcome };
390
+ });
391
+ }
392
+ get rateState() { return { desired: { ...this.desired }, applied: { ...this.applied } }; }
393
+ async checkHealth() {
394
+ await this.provider.options.validateSession?.();
395
+ await this.operationTail;
396
+ await this.command('status');
397
+ if (this.lastCounters?.inputAvailable === false) {
398
+ this.events.push({ type: 'recovery', result: { kind: 'input', verified: false } });
399
+ throw new NativeCommandError('unavailable');
400
+ }
401
+ }
402
+ get counters() { return Object.fromEntries(totals.map(k => [k, this.life.offsets[k] + (this.lastCounters?.[k] ?? 0)])); }
403
+ configured() {
404
+ // Capture-only options belong to prepare, not the configured wire message.
405
+ const { cursorVisible: _cursorVisible, ...selection } = this.selection;
406
+ return { ...selection, deviceGeneration: this.identity.deviceGeneration, controlGeneration: this.identity.controlGeneration,
407
+ mediaEpoch: this.epoch, displayRevision: this.displayRevision, encoderBackend: this.provider.options.backend ?? 'libx264', rateTargetBps: this.applied.targetBps,
408
+ fpsCeiling: this.codecFpsCeiling };
409
+ }
410
+ publishConfigured() { this.events.push({ type: 'configured', configured: this.configured() }); }
411
+ async command(name, payload = {}) {
412
+ if (['input', 'held_state', 'clipboard', 'focus', 'resize'].includes(name))
413
+ await this.provider.options.validateSession?.();
414
+ mediaRequire(!this.stopped && this.life.session === this);
415
+ const admission = name === 'set_rate' ? { ...payload } : undefined;
416
+ const current = () => {
417
+ if (admission && this.captureEpoch !== admission.epoch)
418
+ admission.pauseCapture = true;
419
+ return !this.stopped && this.life.session === this;
420
+ };
421
+ current();
422
+ const reply = await this.helper.request(name, { ...this.identity }, admission ?? payload, current);
423
+ this.lastCounters = reply.counters;
424
+ this.life.lastAppliedInputSequence = Math.max(this.life.lastAppliedInputSequence, reply.counters.lastInputSequence ?? 0);
425
+ mediaRequire(!this.stopped && this.life.session === this);
426
+ return reply.result;
427
+ }
428
+ async cacheDependency(request) {
429
+ return this.serialized(async () => {
430
+ mediaRequire(request.mediaEpoch === this.epoch && request.controlGeneration === this.identity.controlGeneration && request.displayRevision === this.displayRevision);
431
+ const result = await this.command('cache_dependency', { epoch: request.mediaEpoch, exactSequence: request.exactSequence, cacheId: request.cacheId });
432
+ const dependency = z.strictObject({ cacheId: z.literal(request.cacheId), width: z.literal(request.width), height: z.literal(request.height),
433
+ pixels: z.array(z.number().int().min(0).max(255)).length(request.width * request.height * 4) }).parse(result);
434
+ const pixels = Buffer.from(dependency.pixels);
435
+ mediaRequire(pixels.every((byte, i) => i % 4 !== 3 || byte === 255));
436
+ return pixels.toString('base64');
437
+ });
438
+ }
439
+ async accountTransportCost(cost) {
440
+ return this.serialized(async () => {
441
+ mediaRequire(/^[a-f0-9]{32}$/.test(cost.id) && Number.isSafeInteger(cost.totalBytes) && cost.totalBytes >= 0);
442
+ const previous = this.life.transportCosts.get(cost.id) ?? 0;
443
+ mediaRequire(cost.totalBytes >= previous);
444
+ if (!this.life.transportCosts.has(cost.id)) {
445
+ // rateTick owns one report at a time; retired peers cannot enqueue a
446
+ // later report behind eight subsequent replacements.
447
+ if (this.life.transportCosts.size === 8)
448
+ this.life.transportCosts.delete(this.life.transportCosts.keys().next().value);
449
+ this.life.transportCosts.set(cost.id, 0);
450
+ }
451
+ await this.restoreTransportDebt();
452
+ const total = this.transportTotal + cost.totalBytes - previous;
453
+ mediaRequire(Number.isSafeInteger(total));
454
+ if (total !== this.transportTotal)
455
+ await this.command('transport_cost', { totalBytes: total });
456
+ this.transportTotal = total;
457
+ this.life.transportCosts.set(cost.id, cost.totalBytes);
458
+ });
459
+ }
460
+ async restoreTransportDebt() {
461
+ if (this.transportRestored)
462
+ return;
463
+ this.transportTotal = Math.ceil(this.life.transportDebtBits / 8);
464
+ if (this.transportTotal)
465
+ await this.command('transport_cost', { totalBytes: this.transportTotal });
466
+ this.transportRestored = true;
467
+ }
468
+ async start() {
469
+ await this.restoreTransportDebt();
470
+ const reserved = this.reserved;
471
+ this.captureEpoch = reserved.epoch;
472
+ try {
473
+ if (!this.started) {
474
+ const result = await this.command('start', { viewerReady: true, mediaBound: true });
475
+ this.accepted(result.accepted, this.applied.targetBps, this.applied.fpsCeiling);
476
+ this.started = true;
477
+ }
478
+ else {
479
+ await this.command('set_rate', { ...this.applied, pauseCapture: false, reopenEpoch: false, epoch: this.epoch });
480
+ this.applied.pauseCapture = false;
481
+ }
482
+ mediaRequire(this.reserved === reserved);
483
+ }
484
+ catch (error) {
485
+ if (this.reserved === reserved)
486
+ this.captureEpoch = 0;
487
+ throw error;
488
+ }
489
+ }
490
+ accepted(value, targetBps, fpsCeiling, verified = false) {
491
+ const schema = z.object({ backend: z.literal(this.provider.options.backend ?? 'libx264'), representation: z.literal(1),
492
+ width: z.literal(this.selection.width), height: z.literal(this.selection.height), fpsCeiling: z.union([z.literal(15), z.literal(30), z.literal(60)]),
493
+ targetBps: z.number().int().min(1).max(20000000), maxRateBps: z.number().int().min(1), bufferBits: z.number().int().min(1),
494
+ maxBFrames: z.literal(0), qualifiedCap: z.boolean(), runtimeRateReconfigure: z.boolean().optional(),
495
+ rateAdapter: z.string().optional() });
496
+ const actual = schema.parse(value);
497
+ mediaRequire(actual.targetBps === targetBps && actual.fpsCeiling === fpsCeiling && actual.maxRateBps <= targetBps &&
498
+ actual.bufferBits <= targetBps / 4 && actual.representation === 1);
499
+ const runtimeAdapter = this.provider.options.backend === 'videotoolbox' ? 'videotoolbox-data-rate-runtime-v1' : 'x264-vbv-runtime-v1';
500
+ if (verified) {
501
+ mediaRequire(actual.qualifiedCap);
502
+ this.runtimeRate = actual.runtimeRateReconfigure === true && actual.rateAdapter === runtimeAdapter;
503
+ }
504
+ return actual;
505
+ }
506
+ async credit(value) {
507
+ // Credits unblock boundary drains and must never wait on the boundary operation.
508
+ mediaRequire(!this.stopped && this.life.session === this);
509
+ const binding = { ...this.identity };
510
+ const current = () => !this.stopped && this.life.session === this && this.identity.displayRevision === binding.displayRevision &&
511
+ (this.helper.counters ?? this.lastCounters)?.epoch === value.epoch;
512
+ if (!current())
513
+ return;
514
+ let reply;
515
+ try {
516
+ reply = await this.helper.request('credit', binding, value, current);
517
+ }
518
+ catch (error) {
519
+ if (!current())
520
+ return;
521
+ throw error;
522
+ }
523
+ if (!current())
524
+ return;
525
+ this.lastCounters = reply.counters;
526
+ this.life.lastAppliedInputSequence = Math.max(this.life.lastAppliedInputSequence, reply.counters.lastInputSequence ?? 0);
527
+ }
528
+ /** Settle the pipe packet only after actual bounded transport admission and its ownership fence. */
529
+ mediaForwarded(packet) {
530
+ if (this.stopped || this.life.session !== this)
531
+ return;
532
+ const key = this.packetKey(packet), pending = this.pendingPackets.get(key);
533
+ mediaRequire(pending && pending.epoch === packet.epoch && pending.displayRevision === packet.displayRevision && pending.sequence === packet.sequence && pending.type === packet.type &&
534
+ pending.frameId === packet.frameId && pending.fragmentIndex === packet.fragmentIndex);
535
+ this.pendingPackets.delete(key);
536
+ this.pendingPacketBytes -= pending.payload.length;
537
+ if (packet.fragmentIndex + 1 === packet.fragmentCount) {
538
+ if (packet.type === 2)
539
+ this.forwardingUnit = false;
540
+ if (packet.type === 3)
541
+ this.forwardingCursor = false;
542
+ if (packet.type === 2 || packet.type === 5)
543
+ this.forwardedFrame = Math.max(this.forwardedFrame, packet.frameId);
544
+ }
545
+ this.drainWake?.();
546
+ }
547
+ async prepareMotionCut(value) {
548
+ mediaRequire(value.epoch === this.epoch && Number.isSafeInteger(value.cutId) && value.cutId > 0 && !this.stopped && this.life.session === this);
549
+ if (this.motionCut) {
550
+ if (this.motionCut.fence.epoch === value.epoch && this.motionCut.fence.cutId === value.cutId)
551
+ return this.motionCut.fence;
552
+ mediaRequire(this.motionCut.committed && (this.motionCut.fence.epoch !== value.epoch || value.cutId > this.motionCut.fence.cutId));
553
+ }
554
+ const deadline = performance.now() + 4500;
555
+ for (;;) {
556
+ try {
557
+ const reply = await this.helper.request('prepare_motion_cut', this.identity, value, () => !this.stopped && this.life.session === this && this.epoch === value.epoch);
558
+ mediaRequire(!this.stopped && this.life.session === this && this.epoch === value.epoch && reply.counters.epoch === value.epoch);
559
+ const fence = z.strictObject({ epoch: z.literal(value.epoch), cutId: z.literal(value.cutId), pictureThrough: z.number().int().min(0).max(0xffffffff), frameThrough: z.number().int().min(1).max(Number.MAX_SAFE_INTEGER) }).parse(reply.result);
560
+ this.lastCounters = reply.counters;
561
+ this.motionCut = { fence, pipeThrough: reply.counters.sentPayloadBytes, settled: false, committed: false };
562
+ return fence;
563
+ }
564
+ catch (error) {
565
+ if (!(error instanceof NativeCommandError) || error.code !== 'busy' || performance.now() >= deadline)
566
+ throw error;
567
+ await delay(10);
568
+ }
569
+ }
570
+ }
571
+ mediaDisposed(packet, fence) {
572
+ const cut = this.motionCut;
573
+ mediaRequire(cut && cut.fence.cutId === fence.cutId && cut.fence.epoch === packet.epoch && packet.type === 2 && packet.sequence <= fence.pictureThrough);
574
+ const key = this.packetKey(packet), pending = this.pendingPackets.get(key);
575
+ mediaRequire(pending && pending.type === 2 && pending.epoch === packet.epoch && pending.displayRevision === packet.displayRevision && pending.frameId === packet.frameId && pending.fragmentIndex === packet.fragmentIndex);
576
+ this.pendingPackets.delete(key);
577
+ this.pendingPacketBytes -= pending.payload.length;
578
+ if (packet.fragmentIndex + 1 === packet.fragmentCount)
579
+ this.forwardingUnit = false;
580
+ this.drainWake?.();
581
+ }
582
+ async settleMotionCut(fence) {
583
+ const cut = this.motionCut;
584
+ mediaRequire(cut && cut.fence.cutId === fence.cutId && cut.fence.epoch === fence.epoch);
585
+ if (cut.settled)
586
+ return;
587
+ const deadline = performance.now() + 4500;
588
+ while (this.readPayloadEpoch !== fence.epoch || this.readPayloadBytes < cut.pipeThrough || [...this.pendingPackets.values()].some(p => p.epoch === fence.epoch && p.type === 2 && p.sequence <= fence.pictureThrough)) {
589
+ mediaRequire(!this.stopped && this.life.session === this && this.epoch === fence.epoch);
590
+ const remaining = deadline - performance.now();
591
+ mediaRequire(remaining > 0);
592
+ try {
593
+ await bounded(new Promise(resolve => { this.drainWake = resolve; }), remaining);
594
+ }
595
+ finally {
596
+ this.drainWake = undefined;
597
+ }
598
+ }
599
+ this.stream.applyMotionCut(fence.pictureThrough, fence.frameThrough);
600
+ this.forwardingUnit = false;
601
+ this.cutSettledFrame = Math.max(this.cutSettledFrame, fence.frameThrough);
602
+ cut.settled = true;
603
+ this.drainWake?.();
604
+ }
605
+ async commitMotionCut(value) {
606
+ const cut = this.motionCut;
607
+ mediaRequire(cut && cut.settled && cut.fence.cutId === value.cutId && cut.fence.epoch === value.epoch && this.epoch === value.epoch);
608
+ if (cut.committed)
609
+ return;
610
+ await this.command('commit_motion_cut', value);
611
+ cut.committed = true;
612
+ }
613
+ async drainForwarded(frameId) {
614
+ if (!this.packetConsumer)
615
+ return;
616
+ const deadline = performance.now() + 4500;
617
+ while (Math.max(this.forwardedFrame, this.cutSettledFrame) < frameId || this.forwardingUnit || this.forwardingCursor || this.pendingPackets.size) {
618
+ mediaRequire(!this.stopped && this.life.session === this);
619
+ const remaining = deadline - performance.now();
620
+ mediaRequire(remaining > 0);
621
+ try {
622
+ await bounded(new Promise(resolve => { this.drainWake = resolve; }), remaining);
623
+ }
624
+ finally {
625
+ this.drainWake = undefined;
626
+ }
627
+ }
628
+ mediaRequire(!this.stopped && this.life.session === this);
629
+ }
630
+ /** Stop new captures, but keep complete encoded units flowing on the old media leg. */
631
+ async pauseAndDrain() {
632
+ const payload = { ...this.applied, pauseCapture: true, reopenEpoch: false, epoch: this.epoch };
633
+ const deadline = performance.now() + 4500;
634
+ do {
635
+ await this.command('set_rate', payload);
636
+ if (this.lastCounters?.pendingPayloadBytes === 0)
637
+ return;
638
+ await delay(10);
639
+ } while (!this.stopped && performance.now() < deadline);
640
+ throw new NativeCommandError('transport_failed');
641
+ }
642
+ async decoderReady(ack) {
643
+ mediaRequire(ack.controlGeneration === this.identity.controlGeneration && ack.displayRevision === this.displayRevision &&
644
+ ack.mediaEpoch === this.epoch && this.configuration && ack.representation === this.configuration.representation &&
645
+ JSON.stringify(ack.parameterSets) === JSON.stringify(this.configuration.parameterSets));
646
+ await this.command('decoder_ready', { epoch: this.epoch, representation: ack.representation, parameterSets: ack.parameterSets });
647
+ }
648
+ async firstPresented(ack) {
649
+ mediaRequire(ack.epoch === this.epoch && this.firstFrame > 0 && ack.frameId >= this.firstFrame);
650
+ if (!this.presented) {
651
+ await this.command('first_presented', { epoch: this.epoch, frameId: this.firstFrame });
652
+ this.presented = true;
653
+ }
654
+ }
655
+ async acknowledge(ack) {
656
+ await this.operationTail;
657
+ if (this.stopped || ack.epoch !== this.epoch || !this.presented)
658
+ return;
659
+ await this.command('acknowledge', ack);
660
+ }
661
+ async renewAuthenticatedLease(renewal) {
662
+ await this.operationTail;
663
+ await this.command('renew', renewal);
664
+ }
665
+ async heldState(snapshot) {
666
+ await this.operationTail;
667
+ if (snapshot.controlGeneration !== this.identity.controlGeneration || snapshot.displayRevision !== this.displayRevision || snapshot.sequence < this.life.inputSequence)
668
+ return;
669
+ if (snapshot.sequence > this.life.inputSequence) {
670
+ await this.command('held_state', { ...snapshot, keys: [], buttons: [], sequence: this.life.inputSequence });
671
+ return;
672
+ }
673
+ await this.command('held_state', snapshot);
674
+ }
675
+ async reserveOverview(value) {
676
+ await this.operationTail;
677
+ if (this.stopped || !this.started || value.epoch !== this.epoch)
678
+ return false;
679
+ mediaRequire(Number.isSafeInteger(value.wireBytes) && value.wireBytes >= 44 && value.wireBytes <= 32808);
680
+ const epoch = this.epoch;
681
+ try {
682
+ const result = await this.command('reserve_overview', value);
683
+ mediaRequire(typeof result.admitted === 'boolean');
684
+ return !this.stopped && this.epoch === epoch && result.admitted;
685
+ }
686
+ catch {
687
+ return false;
688
+ }
689
+ }
690
+ async overview(maxSide) {
691
+ if (!this.selection.captureRegion || !this.started || this.stopped)
692
+ return undefined;
693
+ try {
694
+ const reply = await this.command('overview', { maxSide });
695
+ if (typeof reply.jpeg !== 'string')
696
+ return undefined;
697
+ const jpeg = Buffer.from(reply.jpeg, 'base64');
698
+ mediaRequire(jpeg.length <= 32768 && jpeg.toString('base64') === reply.jpeg);
699
+ return { width: Number(reply.width), height: Number(reply.height), sourceWidth: Number(reply.sourceWidth), sourceHeight: Number(reply.sourceHeight), jpeg };
700
+ }
701
+ catch {
702
+ // An optional image must never retire decoder/input authority.
703
+ return undefined;
704
+ }
705
+ }
706
+ async clipboard(request) {
707
+ mediaRequire(request.controlGeneration === this.identity.controlGeneration && !this.stopped);
708
+ const reply = await this.command('clipboard', request);
709
+ const message = decodeDesktopControl(JSON.stringify({ v: 1, type: 'clipboard_result', sessionId: this.identity.sessionId,
710
+ payload: reply }), 'device');
711
+ mediaRequire(message.type === 'clipboard_result');
712
+ return message.payload;
713
+ }
714
+ async input(event) {
715
+ decodeDesktopControl(JSON.stringify({ v: 1, type: 'input', sessionId: this.identity.sessionId, payload: event }), 'viewer');
716
+ const rejected = { sequence: event.sequence, outcome: 'rejected', ...(event.action === 'text' ? { operationId: event.operationId } : {}) };
717
+ if (this.stopped || event.controlGeneration !== this.identity.controlGeneration || event.displayRevision !== this.displayRevision ||
718
+ event.sequence <= this.life.inputSequence)
719
+ return rejected;
720
+ this.life.inputSequence = event.sequence;
721
+ if (event.action === 'text') {
722
+ const previous = this.life.text.get(event.operationId);
723
+ if (previous)
724
+ return { ...previous, sequence: event.sequence };
725
+ if (this.life.text.size >= 1024 || this.life.pendingText)
726
+ return rejected;
727
+ this.life.text.set(event.operationId, { ...rejected, outcome: 'unknown' });
728
+ this.life.pendingText = event.operationId;
729
+ }
730
+ try {
731
+ let nativeEvent = event;
732
+ if (event.action === 'pointer' || event.action === 'button' || event.action === 'wheel') {
733
+ const region = this.selection.captureRegion;
734
+ const width = region?.sourceWidth ?? this.selection.width;
735
+ const height = region?.sourceHeight ?? this.selection.height;
736
+ if (event.x < 0 || event.x >= width || event.y < 0 || event.y >= height)
737
+ return rejected;
738
+ if (!region && this.effectiveRegion) {
739
+ const source = this.effectiveRegion;
740
+ nativeEvent = { ...event, x: Math.floor(event.x * source.sourceWidth / width),
741
+ y: Math.floor(event.y * source.sourceHeight / height) };
742
+ }
743
+ }
744
+ const result = await this.command('input', nativeEvent);
745
+ if (event.action === 'text' && result.outcome === 'pending')
746
+ return await this.textResult(event.operationId);
747
+ const parsed = decodeDesktopControl(JSON.stringify({ v: 1, type: 'input_result', sessionId: this.identity.sessionId, payload: result }), 'device');
748
+ mediaRequire(parsed.type === 'input_result');
749
+ if (event.action === 'text') {
750
+ this.life.text.set(event.operationId, parsed.payload);
751
+ this.life.pendingText = undefined;
752
+ }
753
+ return parsed.payload;
754
+ }
755
+ catch {
756
+ if (event.action === 'text') {
757
+ this.life.pendingText = undefined;
758
+ return this.life.text.get(event.operationId);
759
+ }
760
+ return rejected;
761
+ }
762
+ }
763
+ async textResult(operationId) {
764
+ const saved = this.life.text.get(operationId);
765
+ mediaRequire(saved);
766
+ if (this.life.pendingText !== operationId)
767
+ return saved;
768
+ const deadline = performance.now() + 5000;
769
+ try {
770
+ while (!this.stopped && performance.now() < deadline) {
771
+ const result = await this.command('text_result', { operationId });
772
+ if (result.outcome !== 'pending') {
773
+ const message = decodeDesktopControl(JSON.stringify({ v: 1, type: 'input_result', sessionId: this.identity.sessionId, payload: result }), 'device');
774
+ mediaRequire(message.type === 'input_result');
775
+ this.life.text.set(operationId, message.payload);
776
+ return message.payload;
777
+ }
778
+ await delay(10);
779
+ }
780
+ }
781
+ catch { /* Preserve UNKNOWN and never replay after interruption. */ }
782
+ finally {
783
+ if (this.life.pendingText === operationId)
784
+ this.life.pendingText = undefined;
785
+ }
786
+ return this.life.text.get(operationId);
787
+ }
788
+ serialized(run) {
789
+ // Control requests (up to 32 in the supervisor) share this queue with the
790
+ // rate controller. Theme sync can overlap a resize and a rate update at
791
+ // first presentation; pending work must wait rather than invalidate it.
792
+ mediaRequire(this.operationCount < 64);
793
+ this.operationCount++;
794
+ const result = this.operationTail.then(run);
795
+ this.operationTail = result.catch(() => { }).finally(() => { this.operationCount--; });
796
+ return result;
797
+ }
798
+ async atBoundary(command, payload, reserved) {
799
+ const deadline = performance.now() + (command === 'set_rate' ? 12000 : 4500);
800
+ do {
801
+ const coalesced = command === 'set_rate' ? { ...payload, ...this.desired, pauseCapture: true, reopenEpoch: true, epoch: reserved.epoch } : payload;
802
+ try {
803
+ const old = { epoch: this.epoch, displayRevision: this.displayRevision };
804
+ const attempt = this.command(command, coalesced);
805
+ this.boundaryAttempt = attempt;
806
+ let result;
807
+ try {
808
+ result = await attempt;
809
+ }
810
+ finally {
811
+ if (this.boundaryAttempt === attempt)
812
+ this.boundaryAttempt = undefined;
813
+ }
814
+ // Native has closed old capture admission and completed its private writes.
815
+ // Its allocation fence becomes proof only after the real WS acknowledgements.
816
+ this.retiringMedia = old;
817
+ await this.drainForwarded(this.lastCounters.lastAllocatedFrameId);
818
+ return result;
819
+ }
820
+ catch (error) {
821
+ if (!(error instanceof NativeCommandError) || !['busy', 'rate_limited'].includes(error.code) || performance.now() >= deadline)
822
+ throw error;
823
+ this.events.push({ type: 'rate_deferred', reason: error.code, reserved });
824
+ await delay(25);
825
+ }
826
+ } while (!this.stopped);
827
+ throw new NativeCommandError('transport_failed');
828
+ }
829
+ async setRate(decision) {
830
+ this.desired = { ...decision };
831
+ return this.serialized(async () => {
832
+ const current = { ...this.desired };
833
+ if (current.targetBps === this.applied.targetBps && current.fpsCeiling === this.applied.fpsCeiling)
834
+ current.reopenEpoch = false;
835
+ if (!current.reopenEpoch && this.captureEpoch === this.epoch &&
836
+ current.targetBps === this.applied.targetBps && current.fpsCeiling === this.applied.fpsCeiling &&
837
+ current.pauseCapture === this.applied.pauseCapture)
838
+ return;
839
+ if (!current.reopenEpoch) {
840
+ if (!this.runtimeRate) {
841
+ if (current.targetBps !== this.applied.targetBps || current.fpsCeiling > this.applied.fpsCeiling)
842
+ return;
843
+ await this.command('set_rate', { ...current, epoch: this.epoch });
844
+ this.applied = { ...current, pauseCapture: this.lastCounters.pauseCapture };
845
+ return;
846
+ }
847
+ const changed = current.targetBps !== this.applied.targetBps || current.fpsCeiling !== this.applied.fpsCeiling;
848
+ const result = await this.command('set_rate', { ...current, epoch: this.epoch });
849
+ if (changed) {
850
+ this.accepted(result.accepted, current.targetBps, current.fpsCeiling, true);
851
+ mediaRequire(result.reopened === false && Number.isSafeInteger(result.appliedAtMonotonicMs));
852
+ this.life.appliedTargetBps = current.targetBps;
853
+ this.life.appliedFps = current.fpsCeiling;
854
+ this.events.push({ type: 'rate_applied', decision: current, epoch: this.epoch, actualTargetBps: current.targetBps,
855
+ actualFpsCeiling: current.fpsCeiling, appliedAtMonotonicMs: result.appliedAtMonotonicMs });
856
+ }
857
+ this.applied = { ...current, pauseCapture: this.lastCounters.pauseCapture };
858
+ return;
859
+ }
860
+ this.captureEpoch = 0;
861
+ const reserved = this.reservedRate ??= this.life.reserve();
862
+ // Pause until the new CONFIG is committed by viewer_ready + Server START.
863
+ const result = await this.atBoundary('set_rate', { ...current, pauseCapture: true, epoch: reserved.epoch }, reserved);
864
+ const applied = z.object({ targetBps: z.number().int().min(500000).max(20000000), fpsCeiling: z.union([z.literal(15), z.literal(30), z.literal(60)]) }).parse(result.accepted);
865
+ const accepted = this.accepted(result.accepted, applied.targetBps, applied.fpsCeiling);
866
+ mediaRequire(Number.isSafeInteger(result.appliedAtMonotonicMs));
867
+ this.applied = { ...current, targetBps: accepted.targetBps, fpsCeiling: accepted.fpsCeiling, pauseCapture: true };
868
+ this.codecFpsCeiling = accepted.fpsCeiling;
869
+ this.life.appliedTargetBps = accepted.targetBps;
870
+ this.life.appliedFps = accepted.fpsCeiling;
871
+ this.reserved = reserved;
872
+ this.reservedRate = undefined;
873
+ this.configuration = undefined;
874
+ this.firstFrame = 0;
875
+ this.presented = false;
876
+ this.events.push({ type: 'rate_applied', decision: current, epoch: this.epoch, actualTargetBps: this.applied.targetBps,
877
+ actualFpsCeiling: this.applied.fpsCeiling, appliedAtMonotonicMs: result.appliedAtMonotonicMs });
878
+ this.publishConfigured();
879
+ });
880
+ }
881
+ async refresh() {
882
+ return this.serialized(async () => {
883
+ this.captureEpoch = 0;
884
+ const reserved = this.life.reserve();
885
+ await this.pauseAndDrain();
886
+ await this.atBoundary('refresh', { epoch: reserved.epoch }, reserved);
887
+ this.reserved = reserved;
888
+ this.configuration = undefined;
889
+ this.firstFrame = 0;
890
+ this.presented = false;
891
+ this.publishConfigured();
892
+ });
893
+ }
894
+ async setCursorVisible(visible) {
895
+ return this.serialized(async () => {
896
+ mediaRequire(!this.stopped && this.life.session === this);
897
+ const reply = await this.helper.request('cursor_visibility', this.identity, { visible });
898
+ return z.object({ mediaEpoch: z.number().int().min(1), afterFrameId: z.number().int().min(0) }).parse(reply.result);
899
+ });
900
+ }
901
+ async setAppearance(colorScheme) {
902
+ mediaRequire(colorScheme === 'light' || colorScheme === 'dark');
903
+ return this.serialized(async () => {
904
+ mediaRequire(!this.stopped && this.life.session === this && (await this.provider.probe())?.displays[0].appearanceSync === true);
905
+ const { stdout } = await execute(this.provider.displayControl, ['--appearance', colorScheme], { env: this.provider.environment, timeout: 4500, maxBuffer: 4096 });
906
+ z.object({ colorScheme: z.literal(colorScheme), outcome: z.literal('applied') }).parse(JSON.parse(stdout));
907
+ });
908
+ }
909
+ async resize(mode) {
910
+ return this.serialized(() => this.provider.geometryTransaction(async (revision) => {
911
+ this.captureEpoch = 0;
912
+ const reserved = this.life.reserve();
913
+ await this.pauseAndDrain();
914
+ const effectiveRegion = mode.captureRegion ?? (this.effectiveRegion ? {
915
+ x: 0, y: 0, width: this.effectiveRegion.sourceWidth, height: this.effectiveRegion.sourceHeight,
916
+ sourceWidth: this.effectiveRegion.sourceWidth, sourceHeight: this.effectiveRegion.sourceHeight,
917
+ } : undefined);
918
+ const result = await this.atBoundary('resize', { ...mode, ...(effectiveRegion ? { captureRegion: effectiveRegion } : {}),
919
+ ...reserved, expectedDisplayRevision: this.displayRevision, displayRevision: revision }, reserved);
920
+ const axis = z.number().int().min(2).max(4096).refine(n => n % 2 === 0);
921
+ const actual = z.object({ outcome: z.enum(['applied', 'rolled_back']), width: axis, height: axis, displayRevision: z.literal(revision),
922
+ dpi: z.number().int().min(64).max(384).optional(), captureRegion: captureRegion.optional() }).parse(result);
923
+ mediaRequire(canonicalDesktopJson(actual.captureRegion ?? null) === canonicalDesktopJson(effectiveRegion ?? null));
924
+ this.effectiveRegion = effectiveRegion;
925
+ // A helper-only region must not change the identity acknowledged to the server.
926
+ actual.captureRegion = mode.captureRegion;
927
+ if (mode.dpi !== undefined)
928
+ mediaRequire(actual.dpi !== undefined && (actual.outcome !== 'applied' || actual.dpi === mode.dpi));
929
+ else if (this.selection.dpi === undefined)
930
+ delete actual.dpi;
931
+ this.identity.displayRevision = actual.displayRevision;
932
+ this.reserved = reserved;
933
+ this.focusRevision = 1;
934
+ this.selection = { ...this.selection, captureRegion: actual.captureRegion, width: actual.width, height: actual.height, ...(actual.dpi === undefined ? {} : { dpi: actual.dpi }) };
935
+ this.configuration = undefined;
936
+ this.firstFrame = 0;
937
+ this.presented = false;
938
+ this.publishConfigured();
939
+ return { ...actual, commandId: this.selection.commandId };
940
+ }, mode.captureRegion !== undefined || this.effectiveRegion !== undefined));
941
+ }
942
+ /** Keeps server command identities without extending the public provider interface. */
943
+ configurationCommand(commandId) { this.selection = { ...this.selection, commandId }; }
944
+ get codec() { return this.configuration; }
945
+ get packets() {
946
+ const self = this;
947
+ return { async *[Symbol.asyncIterator]() {
948
+ mediaRequire(!self.packetConsumer);
949
+ self.packetConsumer = true;
950
+ let skipCursor = false;
951
+ for await (const bytes of self.helper.packets) {
952
+ if (self.stopped || self.life.session !== self)
953
+ return;
954
+ const reserved = self.reserved;
955
+ const packet = decodeMedia(bytes), retired = self.retiringMedia;
956
+ const old = retired && packet.epoch === retired.epoch && packet.displayRevision === retired.displayRevision;
957
+ mediaRequire((packet.epoch === self.epoch && packet.displayRevision === self.displayRevision) || (old && (packet.type === 3 || packet.type === 4)));
958
+ self.life.lastAppliedInputSequence = Math.max(self.life.lastAppliedInputSequence, packet.lastInputSequence);
959
+ if (self.readPayloadEpoch !== packet.epoch) {
960
+ self.readPayloadEpoch = packet.epoch;
961
+ self.readPayloadBytes = 0;
962
+ }
963
+ self.readPayloadBytes += packet.payload.length;
964
+ mediaRequire(Number.isSafeInteger(self.readPayloadBytes));
965
+ const unit = self.stream.accept(bytes);
966
+ if (packet.fragmentIndex === 0)
967
+ skipCursor = !!old && (packet.type === 3 || packet.type === 4) && !self.forwardingCursor;
968
+ if (skipCursor)
969
+ continue;
970
+ if (packet.type === 1) {
971
+ self.retiringMedia = undefined;
972
+ // Native can verify SPS only after the authorized first encode. Reuse the
973
+ // existing non-reopening response, retaining the current admission state.
974
+ const result = await self.command('set_rate', { ...self.applied, epoch: self.epoch, reopenEpoch: false });
975
+ if (self.stopped || self.life.session !== self || self.reserved !== reserved)
976
+ continue;
977
+ self.accepted(result.accepted, self.applied.targetBps, self.applied.fpsCeiling, true);
978
+ self.configuration = self.stream.configuration;
979
+ }
980
+ if (unit && !self.firstFrame)
981
+ self.firstFrame = unit.frameId;
982
+ const key = self.packetKey(packet);
983
+ mediaRequire(!self.pendingPackets.has(key) && self.pendingPackets.size < 128 && self.pendingPacketBytes + packet.payload.length <= 524288);
984
+ self.pendingPackets.set(key, packet);
985
+ self.pendingPacketBytes += packet.payload.length;
986
+ if (packet.type === 2 && packet.fragmentCount > 1)
987
+ self.forwardingUnit = true;
988
+ if (packet.type === 3 && packet.fragmentCount > 1)
989
+ self.forwardingCursor = true;
990
+ self.drainWake?.();
991
+ yield bytes;
992
+ }
993
+ } };
994
+ }
995
+ stop() {
996
+ if (this.stopPromise)
997
+ return this.stopPromise;
998
+ this.stopped = true;
999
+ this.drainWake?.();
1000
+ return this.stopPromise = (async () => {
1001
+ if (this.life.pendingText)
1002
+ this.life.pendingText = undefined;
1003
+ await this.helper.stop(this.identity);
1004
+ const last = this.helper.counters ?? this.lastCounters;
1005
+ this.life.transportDebtBits = last?.rateBudget?.debtBits ?? this.life.transportDebtBits;
1006
+ for (const k of totals) {
1007
+ const next = this.life.offsets[k] + (last?.[k] ?? 0);
1008
+ mediaRequire(Number.isSafeInteger(next));
1009
+ this.life.offsets[k] = next;
1010
+ }
1011
+ this.lastCounters = undefined;
1012
+ this.life.touched = performance.now();
1013
+ if (this.life.session === this)
1014
+ this.life.session = undefined;
1015
+ this.events.close();
1016
+ })();
1017
+ }
1018
+ }