@platless/axom-cli 0.1.0 → 0.5.9

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 (281) hide show
  1. package/README.md +1 -1
  2. package/dist/authScopes.js +19 -0
  3. package/dist/axom-patch/cli/README.md +132 -0
  4. package/dist/axom-patch/cli/account-limits/README.md +27 -0
  5. package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
  6. package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
  7. package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
  8. package/dist/axom-patch/cli/account-limits/generate.py +120 -0
  9. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
  10. package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
  11. package/dist/axom-patch/cli/claude-account-limits.json +37 -0
  12. package/dist/axom-patch/cli/codex-account-limits.json +37 -0
  13. package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
  14. package/dist/axom-patch/cli/nightly-task.md +152 -0
  15. package/dist/axom-patch/cli/pi-steering.json +37 -0
  16. package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
  17. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +38 -0
  18. package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
  19. package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
  20. package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
  21. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +61 -0
  22. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
  23. package/dist/axom-patch/cli/qoder-background-terminal.md +82 -0
  24. package/dist/axom-patch/cli/qoder-foreground-output.md +42 -0
  25. package/dist/axom-patch/cli/qoder-steering.json +52 -0
  26. package/dist/axom-patch/cli/qoder-steering.md +30 -0
  27. package/dist/axom-patch/cli/qoder-subagent-origin.json +73 -0
  28. package/dist/axom-patch/cli/qoder-subagent-subwork.md +73 -0
  29. package/dist/axom-patch/cli/runtime.json +63 -0
  30. package/dist/axom-patch/cli/steering/claude-steering-js.patch +89 -0
  31. package/dist/axom-patch/cli/steering/claude-steering-types.patch +28 -0
  32. package/dist/axom-patch/cli/steering/codex-steering.patch +57 -0
  33. package/dist/axom-patch/cli/steering/pi-steering.js +79 -0
  34. package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
  35. package/dist/axom-patch/cli/zcode-models.json +38 -0
  36. package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
  37. package/dist/cli.js +36 -1
  38. package/dist/commands/auth.js +4 -13
  39. package/dist/commands/execute.js +4 -4
  40. package/dist/commands/record.js +6 -6
  41. package/dist/commands/struct.js +4 -4
  42. package/dist/commands/workstation.js +2 -2
  43. package/dist/credentials.js +2 -2
  44. package/dist/install/claude/commands.js +13 -13
  45. package/dist/keepers/acp/keeper.mjs +3083 -0
  46. package/dist/keepers/acp/manifest.json +9 -0
  47. package/dist/keepers/terminal/keeper.mjs +8469 -0
  48. package/dist/keepers/terminal/manifest.json +9 -0
  49. package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  50. package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  51. package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
  52. package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
  53. package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
  54. package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
  55. package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
  56. package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
  57. package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
  58. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
  59. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
  60. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
  61. package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  62. package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
  63. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  64. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  65. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  66. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  67. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  68. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  69. package/dist/mcp.js +32 -14
  70. package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
  71. package/dist/runtime-deps/qoder-sdk/package.json +8 -0
  72. package/dist/test/keeperKit.js +67 -0
  73. package/dist/workstation/acp/acpAgents.js +2514 -681
  74. package/dist/workstation/acp/acpClient.js +95 -318
  75. package/dist/workstation/acp/activityBusy.js +43 -0
  76. package/dist/workstation/acp/activityReporting.js +54 -0
  77. package/dist/workstation/acp/adoption.js +28 -356
  78. package/dist/workstation/acp/authUrlSniffer.js +36 -0
  79. package/dist/workstation/acp/binaryInstall.js +100 -27
  80. package/dist/workstation/acp/builds/gc.js +245 -0
  81. package/dist/workstation/acp/builds/install/binary.js +28 -0
  82. package/dist/workstation/acp/builds/install/npm.js +235 -0
  83. package/dist/workstation/acp/builds/install/uv.js +254 -0
  84. package/dist/workstation/acp/builds/migrate.js +195 -0
  85. package/dist/workstation/acp/builds/model.js +128 -0
  86. package/dist/workstation/acp/builds/nodeGate.js +100 -0
  87. package/dist/workstation/acp/builds/store.js +352 -0
  88. package/dist/workstation/acp/commandTableProbe.js +68 -0
  89. package/dist/workstation/acp/configOptions.js +103 -1
  90. package/dist/workstation/acp/delegationMcp.js +24 -0
  91. package/dist/workstation/acp/envSecrets.js +27 -0
  92. package/dist/workstation/acp/initializeResponse.js +285 -0
  93. package/dist/workstation/acp/installErrors.js +12 -2
  94. package/dist/workstation/acp/installProbe.js +1 -1
  95. package/dist/workstation/acp/managedPatches.js +144 -0
  96. package/dist/workstation/acp/outbox.js +330 -0
  97. package/dist/workstation/acp/patchAssets.js +80 -0
  98. package/dist/workstation/acp/permission.js +335 -37
  99. package/dist/workstation/acp/processor/checkpoint.js +87 -0
  100. package/dist/workstation/acp/processor/commands.js +35 -0
  101. package/dist/workstation/acp/processor/control.js +355 -0
  102. package/dist/workstation/acp/processor/emit.js +26 -0
  103. package/dist/workstation/acp/processor/jsonrpc.js +172 -0
  104. package/dist/workstation/acp/processor/memoryLog.js +49 -0
  105. package/dist/workstation/acp/processor/origins.js +48 -0
  106. package/dist/workstation/acp/processor/output.js +67 -0
  107. package/dist/workstation/acp/processor/projection.js +42 -0
  108. package/dist/workstation/acp/processor/record.js +128 -0
  109. package/dist/workstation/acp/processor/runtime.js +181 -0
  110. package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
  111. package/dist/workstation/acp/processor/session.js +525 -0
  112. package/dist/workstation/acp/processor/sideChannel.js +80 -0
  113. package/dist/workstation/acp/processor/state.js +74 -0
  114. package/dist/workstation/acp/processor/step.js +459 -0
  115. package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
  116. package/dist/workstation/acp/processor/test/harness.js +120 -0
  117. package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
  118. package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
  119. package/dist/workstation/acp/processor/transport/generations.js +76 -0
  120. package/dist/workstation/acp/processor/transport/keeper.js +130 -0
  121. package/dist/workstation/acp/processor/transport/local.js +269 -0
  122. package/dist/workstation/acp/processor/transport/types.js +78 -0
  123. package/dist/workstation/acp/processor/updates.js +364 -0
  124. package/dist/workstation/acp/processorSession.js +1512 -0
  125. package/dist/workstation/acp/qoderStartup.js +45 -0
  126. package/dist/workstation/acp/registry/catalog.js +385 -4
  127. package/dist/workstation/acp/registry/detect.js +55 -0
  128. package/dist/workstation/acp/registry/launch.js +61 -119
  129. package/dist/workstation/acp/sessionUpdateMapper.js +56 -27
  130. package/dist/workstation/acp/steering.js +58 -0
  131. package/dist/workstation/acp/switching.js +114 -0
  132. package/dist/workstation/acp/terminalLogin.js +171 -0
  133. package/dist/workstation/acp/terminalLoginSpec.js +41 -0
  134. package/dist/workstation/acp/test/adapterFixtures.js +256 -0
  135. package/dist/workstation/acp/test/daemonLife.js +106 -0
  136. package/dist/workstation/acp/test/fakeAgent.js +185 -9
  137. package/dist/workstation/acp/test/keeperWorld.js +267 -0
  138. package/dist/workstation/acp/test/memoryOutbox.js +55 -0
  139. package/dist/workstation/acp/test/raw/fixtures.js +196 -0
  140. package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
  141. package/dist/workstation/acp/test/raw/sanitize.js +344 -0
  142. package/dist/workstation/acp/test/raw/sources.js +249 -0
  143. package/dist/workstation/acp/test/realAdapter.js +377 -0
  144. package/dist/workstation/acp/test/scriptAgent.js +206 -0
  145. package/dist/workstation/acp/test/sessionFrames.js +42 -0
  146. package/dist/workstation/acp/timedCommand.js +4 -2
  147. package/dist/workstation/acp/uvxInstall.js +72 -122
  148. package/dist/workstation/acp/validate.js +33 -5
  149. package/dist/workstation/agentFlow.js +103 -0
  150. package/dist/workstation/agentSessions.js +1686 -348
  151. package/dist/workstation/channels/agent.js +14 -6
  152. package/dist/workstation/channels/computer.js +203 -7
  153. package/dist/workstation/channels/computerExecs.js +200 -0
  154. package/dist/workstation/channels/management.js +86 -6
  155. package/dist/workstation/config.js +3 -1
  156. package/dist/workstation/connection/encryptedChannel.js +88 -21
  157. package/dist/workstation/daemon.js +309 -6
  158. package/dist/workstation/daemonctl.js +28 -2
  159. package/dist/workstation/desktop/binding.js +36 -0
  160. package/dist/workstation/desktop/bridge.js +212 -0
  161. package/dist/workstation/desktop/flow-control.js +52 -0
  162. package/dist/workstation/desktop/focused-text.js +130 -0
  163. package/dist/workstation/desktop/protocol.js +35 -0
  164. package/dist/workstation/exec/attach.js +126 -0
  165. package/dist/workstation/exec/command.js +40 -0
  166. package/dist/workstation/exec/durable.js +517 -0
  167. package/dist/workstation/exec/fallback.js +106 -0
  168. package/dist/workstation/exec/policy.js +837 -0
  169. package/dist/workstation/exec/shaping.js +211 -0
  170. package/dist/workstation/exec.js +3 -112
  171. package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
  172. package/dist/workstation/files/adapters/toolRequests.js +121 -0
  173. package/dist/workstation/files/client.js +455 -0
  174. package/dist/workstation/files/errors.js +28 -0
  175. package/dist/workstation/files/fence.js +350 -0
  176. package/dist/workstation/files/find/fileIndex.js +153 -0
  177. package/dist/workstation/files/find/fuzzy.js +155 -0
  178. package/dist/workstation/files/host.js +122 -0
  179. package/dist/workstation/files/hostProtocol.js +2 -0
  180. package/dist/workstation/files/media/cache.js +620 -0
  181. package/dist/workstation/files/media/encoding.js +308 -0
  182. package/dist/workstation/files/media/estimator.js +100 -0
  183. package/dist/workstation/files/media/media.js +325 -0
  184. package/dist/workstation/files/media/packedAudio.js +23 -0
  185. package/dist/workstation/files/media/scheduler.js +551 -0
  186. package/dist/workstation/files/media/types.js +5 -0
  187. package/dist/workstation/files/scope.js +45 -0
  188. package/dist/workstation/files/structured.js +284 -0
  189. package/dist/workstation/files/text.js +356 -0
  190. package/dist/workstation/files/uploads.js +351 -0
  191. package/dist/workstation/journal.js +65 -236
  192. package/dist/workstation/keepers/acp/client.js +101 -0
  193. package/dist/workstation/keepers/acp/envelope.js +62 -0
  194. package/dist/workstation/keepers/acp/host.js +419 -0
  195. package/dist/workstation/keepers/acp/main.js +18 -0
  196. package/dist/workstation/keepers/acp/memoryStream.js +121 -0
  197. package/dist/workstation/keepers/acp/params.js +137 -0
  198. package/dist/workstation/keepers/acp/protocol.js +66 -0
  199. package/dist/workstation/keepers/acp/recover.js +120 -0
  200. package/dist/workstation/keepers/acp/session.js +584 -0
  201. package/dist/workstation/keepers/acp/stderr.js +154 -0
  202. package/dist/workstation/keepers/kit/activity.js +103 -0
  203. package/dist/workstation/keepers/kit/bootstrap.js +164 -0
  204. package/dist/workstation/keepers/kit/buildVersion.js +51 -0
  205. package/dist/workstation/keepers/kit/bundle.js +120 -0
  206. package/dist/workstation/keepers/kit/client.js +432 -0
  207. package/dist/workstation/keepers/kit/identity.js +21 -0
  208. package/dist/workstation/keepers/kit/ipc.js +112 -0
  209. package/dist/workstation/keepers/kit/launchConfig.js +8 -0
  210. package/dist/workstation/keepers/kit/launcher.js +249 -0
  211. package/dist/workstation/keepers/kit/lock.js +123 -0
  212. package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
  213. package/dist/workstation/keepers/kit/paths.js +75 -0
  214. package/dist/workstation/keepers/kit/protocol.js +43 -0
  215. package/dist/workstation/keepers/kit/server.js +518 -0
  216. package/dist/workstation/keepers/kit/streamLog.js +697 -0
  217. package/dist/workstation/keepers/legacy.js +296 -0
  218. package/dist/workstation/keepers/policy.js +576 -0
  219. package/dist/workstation/keepers/stop.js +34 -0
  220. package/dist/workstation/keepers/terminal/client.js +213 -0
  221. package/dist/workstation/keepers/terminal/exec.js +981 -0
  222. package/dist/workstation/keepers/terminal/generations.js +129 -0
  223. package/dist/workstation/keepers/terminal/keeper.js +62 -0
  224. package/dist/workstation/keepers/terminal/main.js +43 -0
  225. package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
  226. package/dist/workstation/keepers/terminal/params.js +27 -0
  227. package/dist/workstation/keepers/terminal/protocol.js +56 -0
  228. package/dist/workstation/keepers/terminal/pty.js +330 -0
  229. package/dist/workstation/oauth/deviceAuthorization.js +21 -12
  230. package/dist/workstation/protocol/accountLimits.js +47 -0
  231. package/dist/workstation/protocol/agent.js +201 -8
  232. package/dist/workstation/protocol/common.js +36 -0
  233. package/dist/workstation/protocol/computer.js +107 -7
  234. package/dist/workstation/protocol/management.js +50 -2
  235. package/dist/workstation/qoderAdapter/backgroundProcess.js +118 -0
  236. package/dist/workstation/qoderAdapter/backgroundTerminal.js +193 -0
  237. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
  238. package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
  239. package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
  240. package/dist/workstation/qoderAdapter/contract.js +78 -0
  241. package/dist/workstation/qoderAdapter/foregroundOutput.js +36 -0
  242. package/dist/workstation/qoderAdapter/history.js +85 -0
  243. package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
  244. package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
  245. package/dist/workstation/qoderAdapter/main.js +95 -0
  246. package/dist/workstation/qoderAdapter/mapper.js +117 -0
  247. package/dist/workstation/qoderAdapter/observerPlugin.js +245 -0
  248. package/dist/workstation/qoderAdapter/observerReceiver.js +187 -0
  249. package/dist/workstation/qoderAdapter/permissions.js +61 -0
  250. package/dist/workstation/qoderAdapter/profile.js +93 -0
  251. package/dist/workstation/qoderAdapter/sdkDriver.js +48 -0
  252. package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
  253. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
  254. package/dist/workstation/qoderAdapter/session.js +319 -0
  255. package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
  256. package/dist/workstation/qoderAdapter/subwork.js +85 -0
  257. package/dist/workstation/qoderAdapter/taskControl.js +38 -0
  258. package/dist/workstation/qoderAdapter/taskLedger.js +163 -0
  259. package/dist/workstation/qoderAdapter/types.js +1 -0
  260. package/dist/workstation/qoderAdapter/usage.js +37 -0
  261. package/dist/workstation/register.js +4 -0
  262. package/dist/workstation/systemMetrics.js +110 -1
  263. package/dist/workstation/tools/browser.js +394 -0
  264. package/dist/workstation/tools/client.js +277 -0
  265. package/dist/workstation/tools/info.js +50 -0
  266. package/dist/workstation/tools/keeperProcess.js +124 -0
  267. package/dist/workstation/tools/router.js +55 -0
  268. package/dist/workstation/tools/terminalRouter.js +222 -0
  269. package/dist/workstation/tools/validation.js +86 -0
  270. package/dist/workstation/update/coordinator.js +32 -0
  271. package/dist/workstation/update/npmInstaller.js +2 -0
  272. package/dist/workstation/update/ptyPreflight.js +45 -0
  273. package/dist/workstation/update/taskRegistry.js +141 -23
  274. package/dist/workstation/update/watchdog.js +66 -0
  275. package/package.json +20 -7
  276. package/skills/axom/SKILL.md +55 -17
  277. package/skills/axom-definition/SKILL.md +32 -32
  278. package/skills/axom-platform/SKILL.md +39 -39
  279. package/dist/workstation/acp/acpSession.js +0 -1047
  280. package/dist/workstation/acp/contract.js +0 -494
  281. package/dist/workstation/acp/draftInterceptor.js +0 -154
@@ -0,0 +1,551 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { MediaCache } from './cache.js';
5
+ import { probeSource, encodeGeneration } from './encoding.js';
6
+ import { MediaEstimator, choosePreparation } from './estimator.js';
7
+ import { DEFAULT_MEDIA_POLICY } from './types.js';
8
+ const id = () => randomUUID().replaceAll('-', '');
9
+ const wire = ({ file, ...object }) => object;
10
+ // The 30 fps preview can end a partial frame before the container timestamp.
11
+ const EOF_FRAME_TOLERANCE_MS = 50;
12
+ const matches = (a, b) => a.version === b.version && a.sourcePath === b.sourcePath && a.snapshot === b.snapshot && a.recipe === b.recipe;
13
+ /** One encoder admission for v1 and v2; source descriptors outlive the short tools transaction. */
14
+ export class MediaConsumerExpiredError extends Error {
15
+ constructor() { super('Media consumer was released or expired'); this.name = 'MediaConsumerExpiredError'; }
16
+ }
17
+ export class MediaScheduler {
18
+ cache;
19
+ directory;
20
+ policy;
21
+ jobs = new Map();
22
+ consumers = new Map();
23
+ finite = [];
24
+ active;
25
+ running;
26
+ closed = false;
27
+ timer;
28
+ now;
29
+ probe;
30
+ encode;
31
+ constructor(cache, directory, policy, deps) {
32
+ this.cache = cache;
33
+ this.directory = directory;
34
+ this.policy = policy;
35
+ this.now = deps.now ?? Date.now;
36
+ this.probe = deps.probe ?? probeSource;
37
+ this.encode = deps.encode ?? encodeGeneration;
38
+ this.timer = setInterval(() => { void this.tick().catch(() => { }); }, 250);
39
+ this.timer.unref();
40
+ }
41
+ static async create(directory, policy = DEFAULT_MEDIA_POLICY, deps = {}) {
42
+ return new MediaScheduler(await MediaCache.open(directory, policy), directory, policy, deps);
43
+ }
44
+ /** Encodes running or admitted to run (the active one plus queued finite preparations): work a host restart would lose. */
45
+ get runningEncodes() { return (this.active ? 1 : 0) + this.finite.length; }
46
+ pending() { return [...this.jobs.values()].filter(j => !j.range && j.state !== 'failed').length + this.finite.length + (this.active && !this.active.job ? 1 : 0); }
47
+ async open(input, key, consumerId, hls) {
48
+ let retained = false;
49
+ try {
50
+ if (this.closed)
51
+ throw new Error('Workstation media stopped');
52
+ const existing = this.consumers.get(consumerId);
53
+ if (existing) {
54
+ if (!matches(existing.job.key, key) || existing.hls !== hls)
55
+ throw new Error('Media consumer identity is already in use');
56
+ existing.expires = this.now() + this.policy.consumerLeaseMs;
57
+ return await this.describe(existing);
58
+ }
59
+ if (this.consumers.size >= 256)
60
+ throw new Error('Media resource limit reached');
61
+ let job = this.jobs.get(key.version);
62
+ if (job && !matches(job.key, key))
63
+ throw new Error('Media source changed');
64
+ if (job?.state === 'failed' && !job.consumers.size) {
65
+ this.jobs.delete(key.version);
66
+ await job.input.close();
67
+ job = undefined;
68
+ }
69
+ if (!job) {
70
+ const generations = await this.cache.lookup(key);
71
+ // Another open may have finished while the filesystem lookup awaited.
72
+ job = this.jobs.get(key.version);
73
+ if (!job) {
74
+ const range = generations.flatMap(g => g.objects).find(o => o.type === 'range' && o.startMs === 0);
75
+ const legacy = range ? null : await this.cache.lookupLegacy(key.legacyVersion ?? key.version);
76
+ if (!range && !legacy && this.pending() >= this.policy.maxJobs)
77
+ throw new Error('Workstation media queue is full; try again shortly');
78
+ job = { key, input, consumers: new Map(), generations, probe: generations.find(g => g.probe)?.probe, range: range ?? legacy?.object, legacy: legacy?.object, state: range || legacy ? 'ready' : 'queued', created: this.now() };
79
+ if (range || legacy)
80
+ job.probe = { kind: legacy?.kind ?? (range.extension === 'mp4' ? 'video' : 'audio'), durationMs: legacy?.durationMs ?? range.endMs, durationAccuracy: 'exact', sourceBytes: 0, codec: 'cached', ffmpegVersion: 'cached' };
81
+ this.jobs.set(key.version, job);
82
+ retained = true;
83
+ }
84
+ }
85
+ const consumer = { id: consumerId, job, hls, position: 0, demand: 0, playing: true, revision: 0, explicit: true, requestedAt: this.now(), expires: this.now() + this.policy.consumerLeaseMs, allowed: new Map() };
86
+ job.consumers.set(consumerId, consumer);
87
+ job.releaseAt = undefined;
88
+ this.consumers.set(consumerId, consumer);
89
+ this.assignCached(consumer);
90
+ this.kick();
91
+ return await this.describe(consumer);
92
+ }
93
+ finally {
94
+ if (!retained)
95
+ await input.close();
96
+ }
97
+ }
98
+ consumer(key, consumerId) {
99
+ const c = this.consumers.get(consumerId);
100
+ if (!c)
101
+ throw new MediaConsumerExpiredError();
102
+ if (!matches(c.job.key, key))
103
+ throw new Error('Media consumer identity mismatch');
104
+ c.expires = this.now() + this.policy.consumerLeaseMs;
105
+ return c;
106
+ }
107
+ async refreshRetained(job) {
108
+ const retained = await this.cache.lookup(job.key);
109
+ const active = this.active?.job === job ? this.active.generation : undefined;
110
+ job.generations = retained.map(g => { if (g.id === active?.id) {
111
+ active.objects = g.objects;
112
+ return active;
113
+ } return g; });
114
+ if (active && !job.generations.some(g => g.id === active.id))
115
+ job.generations.push(active);
116
+ for (const c of job.consumers.values()) {
117
+ if (c.generation)
118
+ c.generation = job.generations.find(g => g.id === c.generation.id);
119
+ if (c.preparing)
120
+ c.preparing = job.generations.find(g => g.id === c.preparing.id);
121
+ }
122
+ if (job.range && !job.legacy && !job.generations.some(g => g.objects.some(o => o.id === job.range.id))) {
123
+ job.range = undefined;
124
+ job.state = 'queued';
125
+ }
126
+ }
127
+ assignCached(c) {
128
+ const generations = c.job.generations.filter(g => g.objects.some(o => o.type === 'segment' && o.startMs <= c.position && (o.endMs > c.position || (c.position === c.job.probe?.durationMs && o.endMs === c.position))));
129
+ if (generations.length) {
130
+ c.generation = generations.at(-1);
131
+ c.explicit = false;
132
+ }
133
+ }
134
+ async status(key, consumerId) { const c = this.consumer(key, consumerId); await this.refreshRetained(c.job); this.kick(); return this.describe(c); }
135
+ async seek(key, consumerId, positionMs, revision) {
136
+ const c = this.consumer(key, consumerId);
137
+ if (!Number.isSafeInteger(positionMs) || positionMs < 0 || positionMs > 21600000 || !Number.isSafeInteger(revision) || revision < 0)
138
+ throw new Error('Invalid media seek');
139
+ if (revision <= c.revision)
140
+ return this.describe(c);
141
+ if (c.job.probe?.durationMs != null && positionMs > c.job.probe.durationMs)
142
+ throw new Error('Invalid media seek');
143
+ await this.refreshRetained(c.job);
144
+ c.revision = revision;
145
+ c.position = positionMs;
146
+ c.demand = positionMs;
147
+ c.explicit = true;
148
+ c.requestedAt = this.now();
149
+ c.generation = undefined;
150
+ c.preparing = undefined;
151
+ c.playlist = undefined;
152
+ c.allowed.clear();
153
+ await this.cache.unpin(c.id);
154
+ this.assignCached(c);
155
+ this.kick();
156
+ return this.describe(c);
157
+ }
158
+ async interest(key, consumerId, positionMs, playing) {
159
+ const c = this.consumer(key, consumerId);
160
+ if (!Number.isSafeInteger(positionMs) || positionMs < 0 || positionMs > 21600000 || typeof playing !== 'boolean')
161
+ throw new Error('Invalid media interest');
162
+ c.position = positionMs;
163
+ if (!playing)
164
+ c.demand = positionMs;
165
+ c.playing = playing;
166
+ this.kick();
167
+ }
168
+ async object(key, consumerId, objectId, offset, length) {
169
+ const c = this.consumer(key, consumerId);
170
+ if ((c.allowed.get(objectId) ?? 0) <= Date.now())
171
+ throw new Error('Media object is not available to this consumer');
172
+ const object = c.job.range?.id === objectId ? c.job.range : c.job.generations.flatMap(g => g.objects).find(o => o.id === objectId);
173
+ if (!object)
174
+ throw new Error('Media object is unavailable');
175
+ let bytes;
176
+ if (c.job.legacy?.id === objectId) {
177
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(length) || length < 1 || length > 256 * 1024)
178
+ throw new Error('Invalid media read');
179
+ const handle = await fs.open(object.file, constants.O_RDONLY | constants.O_NOFOLLOW);
180
+ try {
181
+ const stat = await handle.stat();
182
+ if (!stat.isFile() || stat.size !== object.size)
183
+ throw new Error('Media cache changed');
184
+ bytes = Buffer.alloc(Math.min(length, Math.max(0, object.size - offset)));
185
+ const r = await handle.read(bytes, 0, bytes.length, offset);
186
+ bytes = bytes.subarray(0, r.bytesRead);
187
+ }
188
+ finally {
189
+ await handle.close();
190
+ }
191
+ }
192
+ else
193
+ bytes = await this.cache.read(key, objectId, offset, length);
194
+ if (c.playing)
195
+ c.demand = Math.max(c.demand, object.endMs);
196
+ this.kick();
197
+ return { objectId, offset, size: object.size, etag: object.etag, data: bytes.toString('base64'), eof: offset + bytes.length >= object.size };
198
+ }
199
+ /** Cleanup only: opaque consumer/version owns the job even if its source/root disappeared. */
200
+ async releaseIdentity(version, consumerId) {
201
+ const c = this.consumers.get(consumerId);
202
+ if (!c)
203
+ return;
204
+ if (c.job.key.version !== version)
205
+ throw new Error('Media consumer identity mismatch');
206
+ await this.release(c.job.key, consumerId);
207
+ }
208
+ async release(key, consumerId) {
209
+ const c = this.consumers.get(consumerId);
210
+ if (!c)
211
+ return;
212
+ if (!matches(c.job.key, key))
213
+ throw new Error('Media consumer identity mismatch');
214
+ this.consumers.delete(c.id);
215
+ c.job.consumers.delete(c.id);
216
+ await this.cache.unpin(c.id);
217
+ if (!c.job.consumers.size)
218
+ c.job.releaseAt = this.now() + this.policy.releaseGraceMs;
219
+ }
220
+ async tick() {
221
+ const now = this.now();
222
+ for (const c of [...this.consumers.values()])
223
+ if (c.expires <= now)
224
+ await this.release(c.job.key, c.id);
225
+ for (const job of [...this.jobs.values()])
226
+ if (job.releaseAt !== undefined && job.releaseAt <= now) {
227
+ if (this.active?.job === job) {
228
+ this.active.stopped = true;
229
+ this.active.controller.abort();
230
+ }
231
+ else {
232
+ this.jobs.delete(job.key.version);
233
+ await job.input.close();
234
+ }
235
+ }
236
+ this.kick();
237
+ }
238
+ runFinite(run, signal) {
239
+ if (this.closed)
240
+ return Promise.reject(new Error('Workstation media stopped'));
241
+ if (this.pending() >= this.policy.maxJobs)
242
+ return Promise.reject(new Error('Workstation media queue is full'));
243
+ return new Promise((resolve, reject) => {
244
+ const controller = new AbortController();
245
+ const task = { run, controller, resolve: value => resolve(value), reject, cleanup: () => signal.removeEventListener('abort', abort) };
246
+ const abort = () => { controller.abort(); const index = this.finite.indexOf(task); if (index >= 0) {
247
+ this.finite.splice(index, 1);
248
+ task.cleanup();
249
+ reject(new Error('Media preparation cancelled'));
250
+ } };
251
+ signal.addEventListener('abort', abort, { once: true });
252
+ this.finite.push(task);
253
+ if (signal.aborted)
254
+ abort();
255
+ this.kick();
256
+ });
257
+ }
258
+ fullGeneration(job) {
259
+ return job.generations.find(g => {
260
+ if (!g.complete || !g.sourceComplete || g.sourceOffsetMs !== 0)
261
+ return false;
262
+ let end = 0;
263
+ const objects = g.objects.filter(o => o.type === 'segment').sort((a, b) => a.startMs - b.startMs);
264
+ for (const o of objects) {
265
+ if (o.startMs > end + 1)
266
+ return false;
267
+ end = Math.max(end, o.endMs);
268
+ }
269
+ return objects.length > 0 && end >= (job.probe?.durationMs ?? objects.at(-1).endMs) - EOF_FRAME_TOLERANCE_MS;
270
+ });
271
+ }
272
+ candidate(c) {
273
+ const j = c.job;
274
+ if (j.range || j.state === 'failed' || !j.consumers.size)
275
+ return;
276
+ if (!c.mode && !j.finiteUnavailable && this.fullGeneration(j))
277
+ return { job: j, consumer: c, start: 0, rank: 0, requestedAt: c.requestedAt, revision: c.revision };
278
+ const end = this.frontier(c);
279
+ const desired = c.playing ? Math.max(c.position, c.demand) : c.position;
280
+ if (j.probe?.durationMs != null && end >= j.probe.durationMs && !c.explicit)
281
+ return;
282
+ if (!c.explicit && j.probe?.durationMs != null && end >= j.probe.durationMs - EOF_FRAME_TOLERANCE_MS && j.generations.some(g => g.complete && g.sourceComplete && g.sourceOffsetMs <= end && (g.objects.at(-1)?.endMs ?? Infinity) <= end))
283
+ return;
284
+ if (c.hls && !c.explicit && end - desired >= this.policy.lookaheadMs)
285
+ return;
286
+ return { job: j, consumer: c, start: c.explicit ? (c.position === j.probe?.durationMs ? Math.max(0, c.position - 4000) : c.position) : end, rank: c.explicit ? 0 : c.playing && end - desired < 12000 ? 1 : 2, requestedAt: c.requestedAt, revision: c.revision };
287
+ }
288
+ frontier(c) {
289
+ // Downloaded data is not a played position. Keep a conservative full lookahead behind
290
+ // demand so background decoders can advance without pinning the entire watched prefix.
291
+ let end = Math.max(c.position, c.playing ? c.demand - this.policy.lookaheadMs : 0);
292
+ const objects = c.job.generations.flatMap(g => g.objects).filter(o => o.type === 'segment').sort((a, b) => a.startMs - b.startMs);
293
+ for (const o of objects) {
294
+ if (o.startMs > end)
295
+ break;
296
+ if (o.endMs > end)
297
+ end = o.endMs;
298
+ }
299
+ return end;
300
+ }
301
+ candidates() { return [...this.consumers.values()].map(c => this.candidate(c)).filter((c) => !!c).sort((a, b) => a.rank - b.rank || a.requestedAt - b.requestedAt); }
302
+ kick() {
303
+ if (this.closed || this.active)
304
+ return;
305
+ const candidate = this.candidates()[0];
306
+ const finite = this.finite[0];
307
+ if (!candidate && !finite)
308
+ return;
309
+ const active = { controller: new AbortController(), stopped: false, started: this.now() };
310
+ this.active = active;
311
+ this.running = (async () => {
312
+ if (finite) {
313
+ this.finite.shift();
314
+ active.controller = finite.controller;
315
+ try {
316
+ finite.resolve(await finite.run(finite.controller.signal));
317
+ }
318
+ catch (error) {
319
+ finite.reject(error);
320
+ }
321
+ finally {
322
+ finite.cleanup();
323
+ }
324
+ }
325
+ else if (candidate) {
326
+ active.job = candidate.job;
327
+ active.target = candidate.consumer;
328
+ await this.produce(candidate, active);
329
+ }
330
+ })().catch(() => { }).finally(async () => {
331
+ if (active.job?.releaseAt !== undefined && active.job.releaseAt <= this.now()) {
332
+ this.jobs.delete(active.job.key.version);
333
+ await active.job.input.close();
334
+ }
335
+ if (this.active === active)
336
+ this.active = undefined;
337
+ this.kick();
338
+ });
339
+ }
340
+ async produce(candidate, active) {
341
+ const job = candidate.job;
342
+ let claim;
343
+ try {
344
+ if (!job.verifiedProbe || !job.probe) {
345
+ job.state = 'probing';
346
+ job.probe = await this.probe(job.input, active.controller.signal);
347
+ job.verifiedProbe = true;
348
+ }
349
+ if (job.probe.kind === 'image')
350
+ throw new Error('Images use completed finite preparation');
351
+ job.estimator ??= await MediaEstimator.load(this.directory, job.probe.ffmpegVersion, job.key.recipe);
352
+ job.estimate = job.estimator.predict(job.probe);
353
+ job.state = 'preparing';
354
+ claim = await this.cache.claimWriter(job.key);
355
+ // Open/probe is asynchronous. A seek received during it supersedes the captured
356
+ // target; it must not be cleared by the old producer's initial assignment.
357
+ if (candidate.consumer.revision !== candidate.revision)
358
+ return;
359
+ const reusable = this.fullGeneration(job);
360
+ if (reusable && !job.finiteUnavailable && !candidate.consumer.mode) {
361
+ job.range = await this.cache.finalizeRange(job.key, reusable.id, active.controller.signal) ?? undefined;
362
+ job.finiteUnavailable = !job.range;
363
+ if (!job.range && [...job.consumers.values()].some(c => !c.hls))
364
+ throw new Error('Not enough cache space for a completed media preview');
365
+ job.state = job.range ? 'ready' : 'streamable';
366
+ return;
367
+ }
368
+ for (const old of job.generations)
369
+ if (!old.complete) {
370
+ old.complete = true;
371
+ old.sourceComplete = false;
372
+ await this.cache.finishGeneration(job.key, old);
373
+ }
374
+ const generation = { id: id(), sourceOffsetMs: candidate.start, timestampOriginMs: 0, targetDurationMs: 4000, objects: [], complete: false, sourceComplete: false, createdAtMs: this.now(), probe: job.probe };
375
+ active.generation = generation;
376
+ job.generations.push(generation);
377
+ const output = await this.cache.createGeneration(job.key, generation.id);
378
+ for (const c of job.consumers.values()) {
379
+ if (c.explicit && (c.position === candidate.start || (c === candidate.consumer && c.revision === candidate.revision))) {
380
+ c.generation = generation;
381
+ c.preparing = undefined;
382
+ c.explicit = false;
383
+ }
384
+ else if (c === candidate.consumer && c.revision === candidate.revision)
385
+ c.preparing = generation;
386
+ }
387
+ const started = this.now();
388
+ let calibrated = false;
389
+ try {
390
+ const result = await this.encode(job.input, job.probe, candidate.start, output, active.controller.signal, async (object) => {
391
+ await this.cache.publish(job.key, generation, object);
392
+ generation.objects.push(object);
393
+ await this.refreshRetained(job);
394
+ for (const c of job.consumers.values())
395
+ if (!c.explicit && c.generation !== generation && c.position < object.endMs && this.frontier(c) >= object.endMs)
396
+ c.preparing = generation;
397
+ if (!calibrated && this.now() > started) {
398
+ calibrated = true;
399
+ await job.estimator.observe(job.probe, { sourceMs: object.endMs - candidate.start, elapsedMs: this.now() - started, sourceBytes: Math.round(job.probe.sourceBytes * (object.endMs - candidate.start) / job.probe.durationMs), finalizationMs: 0 });
400
+ job.estimate = job.estimator.predict(job.probe);
401
+ }
402
+ job.state = 'streamable';
403
+ for (const c of job.consumers.values())
404
+ if (c.generation === generation)
405
+ await this.describe(c);
406
+ const current = [...job.consumers.values()].filter(c => c.generation === generation || c.preparing === generation);
407
+ const end = object.endMs;
408
+ const need = current.some(c => !c.hls || end - (c.playing ? Math.max(c.position, c.demand) : c.position) < this.policy.lookaheadMs);
409
+ const waiting = this.candidates().find(c => c.job !== job || (c.consumer.generation !== generation && c.consumer.preparing !== generation));
410
+ const currentRank = Math.min(...current.map(c => this.candidate(c)?.rank ?? 3), 3);
411
+ const mode = choosePreparation(job.estimate, false, true, this.now() - started);
412
+ const windowComplete = (mode === 'hls-window' || current.some(c => c.mode === 'hls-window')) && end - generation.sourceOffsetMs >= 8000;
413
+ // Stop only at a published boundary. Other consumers retain their immutable generation.
414
+ const finiteRequired = [...job.consumers.values()].some(c => !c.hls);
415
+ const preparingFinite = [...job.consumers.values()].some(c => !c.mode && ['range', 'calibrating'].includes(choosePreparation(job.estimate, false, c.hls, this.now() - started)));
416
+ if (!finiteRequired && end < job.probe.durationMs - EOF_FRAME_TOLERANCE_MS && ((!preparingFinite && ((!need && job.consumers.size) || windowComplete)) || (waiting && waiting.rank < currentRank))) {
417
+ active.stopped = true;
418
+ active.controller.abort();
419
+ }
420
+ }, () => { });
421
+ generation.complete = result.complete;
422
+ generation.sourceComplete = result.sourceComplete;
423
+ }
424
+ catch (error) {
425
+ if (!active.stopped)
426
+ throw error;
427
+ generation.complete = true;
428
+ generation.sourceComplete = false;
429
+ }
430
+ await this.cache.finishGeneration(job.key, generation);
431
+ if (generation.sourceComplete && generation.sourceOffsetMs === 0) {
432
+ const finalized = this.now();
433
+ job.range = await this.cache.finalizeRange(job.key, generation.id, active.controller.signal) ?? undefined;
434
+ if (!job.range && [...job.consumers.values()].some(c => !c.hls))
435
+ throw new Error('Not enough cache space for a completed media preview');
436
+ job.finiteUnavailable = !job.range;
437
+ if (this.now() > started)
438
+ await job.estimator.observe(job.probe, { sourceMs: job.probe.durationMs, elapsedMs: Math.max(1, finalized - started), sourceBytes: job.probe.sourceBytes, finalizationMs: Math.max(0, this.now() - finalized) });
439
+ }
440
+ job.state = job.range ? 'ready' : 'streamable';
441
+ }
442
+ catch (error) {
443
+ if (!active.stopped) {
444
+ job.state = 'failed';
445
+ job.error = error.message.slice(0, 512);
446
+ await job.input.close();
447
+ }
448
+ }
449
+ finally {
450
+ await claim?.release();
451
+ }
452
+ }
453
+ /** Encoder windows are scheduling units, not native-player lifetimes. Append contiguous
454
+ * published windows to one consumer playlist; only an explicit seek replaces its URI. */
455
+ extendPlaylist(c) {
456
+ const playlist = c.playlist;
457
+ const candidates = c.job.generations.flatMap(g => g.objects.filter(o => o.type === 'segment').map(o => ({ g, o })));
458
+ while (playlist.objects.length < 256) {
459
+ const next = candidates.find(({ o }) => o.startMs === playlist.end && o.endMs > playlist.end);
460
+ if (!next)
461
+ break;
462
+ const previous = playlist.objects.at(-1);
463
+ const continuous = previous && next.o.timestamp90k === previous.timestamp90k + (previous.endMs - previous.startMs) * 90;
464
+ playlist.objects.push({ ...next.o, sequence: playlist.nextSequence++, discontinuity: next.g.id !== playlist.lastGeneration && !continuous });
465
+ playlist.lastGeneration = next.g.id;
466
+ playlist.end = next.o.endMs;
467
+ }
468
+ playlist.sourceComplete = c.job.generations.some(g => g.id === playlist.lastGeneration && g.complete && g.sourceComplete && playlist.end >= (c.job.probe?.durationMs ?? Infinity) - EOF_FRAME_TOLERANCE_MS);
469
+ }
470
+ async describe(c) {
471
+ const j = c.job;
472
+ const g = c.generation;
473
+ const probe = j.probe;
474
+ const base = { state: j.state, version: j.key.version, kind: probe?.kind, sourceDurationMs: probe?.durationMs, durationAccuracy: probe?.durationAccuracy,
475
+ estimatedPreparationMs: j.estimate?.serviceMs, queueWaitMs: j.state === 'queued' ? Math.max(0, this.now() - c.requestedAt) : 0,
476
+ ...(j.error ? { error: j.error, errorCode: 'media-preparation-failed' } : {}) };
477
+ // Do not replace an already playing HLS generation with a future finite derivative.
478
+ if (j.range && (!c.hls || (!g && !c.playlist) || !c.mode)) {
479
+ c.allowed.set(j.range.id, Date.now() + 60000);
480
+ await this.cache.pin(c.id, [j.range.id], Date.now() + 60000);
481
+ return { ...base, state: 'ready', delivery: 'range', objects: [wire(j.range)], size: j.range.size, mimeType: j.range.mimeType, extension: j.range.extension, generationComplete: true, sourceComplete: true };
482
+ }
483
+ if (!g && !c.playlist)
484
+ return { ...base, state: j.state === 'failed' ? 'failed' : j.state === 'probing' ? 'probing' : 'queued' };
485
+ const mode = c.mode ?? choosePreparation(j.estimate ?? { serviceMs: null, firstPlayableMs: null, confidence: 'low', producerSpeed: null }, false, c.hls, this.active?.job === j ? this.now() - this.active.started : 0);
486
+ const lower = Math.max(0, c.position - 12000, c.playing ? c.demand - this.policy.lookaheadMs : 0);
487
+ const upper = Math.max(c.position, c.playing ? c.demand : 0) + this.policy.lookaheadMs;
488
+ if (c.playlist) {
489
+ while (c.playlist.objects[0]?.endMs <= lower) {
490
+ if (c.playlist.objects.shift().discontinuity)
491
+ c.playlist.discontinuitySequence++;
492
+ }
493
+ this.extendPlaylist(c);
494
+ }
495
+ const all = c.playlist?.objects ?? g.objects.filter(o => o.type === 'segment');
496
+ const after = all.filter(o => o.endMs > lower);
497
+ let objects = after.filter(o => o.startMs < upper).slice(0, 256);
498
+ const span = objects.length ? objects.at(-1).endMs - objects[0].startMs : 0;
499
+ const streamable = c.hls && objects.length > 0 && (!!c.playlist || (g.complete && (g.sourceOffsetMs > 0 || !g.sourceComplete || !!c.mode || j.finiteUnavailable)) || ((mode === 'hls-growing') && span >= 12000));
500
+ if (streamable) {
501
+ c.mode ??= mode === 'hls-growing' ? 'hls-growing' : 'hls-window';
502
+ if (!c.playlist) {
503
+ c.playlist = { id: g.id, offset: g.sourceOffsetMs, objects: [], end: objects[0].startMs, nextSequence: objects[0].sequence, lastGeneration: g.id, discontinuitySequence: 0, sourceComplete: false };
504
+ this.extendPlaylist(c);
505
+ objects = c.playlist.objects.filter(o => o.endMs > lower && o.startMs < upper);
506
+ }
507
+ }
508
+ for (const [object, expiry] of c.allowed)
509
+ if (expiry <= Date.now())
510
+ c.allowed.delete(object);
511
+ const retainedSpan = objects.length ? objects.at(-1).endMs - objects[0].startMs : 0;
512
+ const retainedUntil = Date.now() + Math.max(16000, retainedSpan + 4000);
513
+ for (const object of objects)
514
+ c.allowed.set(object.id, Math.max(c.allowed.get(object.id) ?? 0, retainedUntil));
515
+ await this.cache.pin(c.id, objects.map(o => o.id), retainedUntil);
516
+ return { ...base, state: j.state === 'failed' ? 'failed' : streamable ? 'streamable' : 'preparing', delivery: 'hls', generationId: c.playlist?.id ?? g.id, generationOffsetMs: c.playlist?.offset ?? g.sourceOffsetMs, targetDurationMs: g?.targetDurationMs ?? 4000,
517
+ generationComplete: c.playlist?.sourceComplete ?? g.complete, sourceComplete: c.playlist?.sourceComplete ?? g.sourceComplete, discontinuitySequence: c.playlist?.discontinuitySequence ?? 0,
518
+ objects: objects.map(wire), encodedIntervals: intervals(j.generations.flatMap(g => g.objects)) };
519
+ }
520
+ async reserveLegacy(output, bytes) { return this.cache.reserveLegacy(output, bytes); }
521
+ async pinLegacy(version) { await this.cache.pin(`legacy:${version}`, [version.slice(0, 32)], Date.now() + 60000); await this.cache.unpin(`legacy:${version}`); }
522
+ async trimCache() { await this.cache.trim(Date.now()); }
523
+ async dispose() {
524
+ this.closed = true;
525
+ clearInterval(this.timer);
526
+ this.active?.controller.abort();
527
+ for (const finite of this.finite) {
528
+ finite.cleanup();
529
+ finite.reject(new Error('Workstation media stopped'));
530
+ }
531
+ this.finite = [];
532
+ await this.running;
533
+ for (const c of this.consumers.values())
534
+ await this.cache.unpin(c.id);
535
+ this.consumers.clear();
536
+ for (const j of this.jobs.values())
537
+ await j.input.close();
538
+ this.jobs.clear();
539
+ }
540
+ }
541
+ function intervals(objects) {
542
+ const merged = [];
543
+ for (const o of objects.filter(o => o.type === 'segment').sort((a, b) => a.startMs - b.startMs)) {
544
+ const last = merged.at(-1);
545
+ if (last && last.endMs >= o.startMs)
546
+ last.endMs = Math.max(last.endMs, o.endMs);
547
+ else
548
+ merged.push({ startMs: o.startMs, endMs: o.endMs });
549
+ }
550
+ return merged.slice(-64);
551
+ }
@@ -0,0 +1,5 @@
1
+ export const DEFAULT_MEDIA_POLICY = {
2
+ finiteThresholdMs: 10_000, maxVersionBytes: 512 * 1024 * 1024, maxCacheBytes: 2 * 1024 * 1024 * 1024,
3
+ ttlMs: 24 * 60 * 60 * 1000, maxJobs: 8, consumerLeaseMs: 60_000, releaseGraceMs: 5_000,
4
+ lookaheadMs: 60_000, noProgressMs: 30_000,
5
+ };
@@ -0,0 +1,45 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { FileError } from './errors.js';
4
+ const OPERATIONS = {
5
+ 'request-root': new Set(['read', 'write', 'list', 'mkdir']),
6
+ 'configured-roots': new Set(['read', 'list', 'find', 'media', 'mkdir', 'rename', 'delete', 'upload']),
7
+ 'read-only': new Set(['read', 'list', 'find', 'media']),
8
+ 'filesystem': new Set(['read', 'list', 'find', 'media', 'mkdir', 'rename', 'delete', 'upload']),
9
+ };
10
+ export const READ_ONLY = Object.freeze({ kind: 'read-only' });
11
+ export async function filesystemScope(home) {
12
+ home = await fs.realpath(home);
13
+ return Object.freeze({ kind: 'filesystem', home, roots: Object.freeze([home, path.parse(home).root]) });
14
+ }
15
+ /** The server-given root must be absolute: a relative one would resolve against whatever directory a section pinned. */
16
+ export function requestRoot(root) {
17
+ if (typeof root !== 'string' || !path.isAbsolute(root) || root.includes('\0')) {
18
+ throw new FileError('invalid_path', 'Workspace root must be an absolute path');
19
+ }
20
+ return Object.freeze({ kind: 'request-root', root });
21
+ }
22
+ /** Canonical home plus canonical workspace roots (each must be a directory), exactly as the keeper computed them. */
23
+ export async function configuredRoots(home, roots) {
24
+ home = await fs.realpath(home);
25
+ const canonical = new Set([home]);
26
+ for (const root of roots) {
27
+ const real = await fs.realpath(root);
28
+ if (!(await fs.stat(real)).isDirectory())
29
+ throw new Error('File root must be a directory');
30
+ canonical.add(real);
31
+ }
32
+ return Object.freeze({ kind: 'configured-roots', roots: Object.freeze([...canonical]), home });
33
+ }
34
+ export function scopeAllows(scope, operation) {
35
+ return OPERATIONS[scope.kind].has(operation);
36
+ }
37
+ export function assertScopeAllows(scope, operation) {
38
+ if (scopeAllows(scope, operation))
39
+ return;
40
+ if (scope.kind === 'read-only')
41
+ throw new FileError('permission_denied', 'Read-only file access cannot modify files');
42
+ throw new FileError('invalid_argument', 'File operation is not available in this scope');
43
+ }
44
+ /** `target` is `root` or below it (both canonical absolute paths). */
45
+ export function inside(target, root) { return target === root || target.startsWith(root + path.sep); }