@hraness/sponge 0.2.6 → 0.3.1

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 (227) hide show
  1. package/README.md +21 -9
  2. package/bin/sponge.js +8 -7
  3. package/dist/contracts/index.js +24 -6
  4. package/dist/contracts/validate.js +106 -29
  5. package/dist/migrations/008.d.ts +3 -0
  6. package/dist/migrations/008.js +5 -0
  7. package/dist/migrations/009.d.ts +3 -0
  8. package/dist/migrations/009.js +8 -0
  9. package/dist/migrations/010.d.ts +3 -0
  10. package/dist/migrations/010.js +11 -0
  11. package/dist/migrations/index.d.ts +15 -0
  12. package/dist/migrations/index.js +39 -0
  13. package/dist/src/acquisition/capture.js +87 -28
  14. package/dist/src/acquisition/cleanup.d.ts +0 -4
  15. package/dist/src/acquisition/cleanup.js +3 -5
  16. package/dist/src/acquisition/errors.js +17 -2
  17. package/dist/src/acquisition/files.d.ts +0 -1
  18. package/dist/src/acquisition/files.js +14 -11
  19. package/dist/src/acquisition/index.d.ts +2 -3
  20. package/dist/src/acquisition/index.js +1 -2
  21. package/dist/src/acquisition/lock.js +1 -1
  22. package/dist/src/acquisition/normalize.js +57 -18
  23. package/dist/src/acquisition/pins.js +22 -4
  24. package/dist/src/acquisition/process.js +67 -29
  25. package/dist/src/acquisition/runtime.d.ts +5 -7
  26. package/dist/src/acquisition/runtime.js +129 -68
  27. package/dist/src/acquisition/verify-helper.js +10 -3
  28. package/dist/src/annotations/quote.js +40 -12
  29. package/dist/src/annotations/store.d.ts +0 -1
  30. package/dist/src/annotations/store.js +66 -14
  31. package/dist/src/app/article.d.ts +2 -1
  32. package/dist/src/app/article.js +66 -70
  33. package/dist/src/app/bundle-files.d.ts +0 -4
  34. package/dist/src/app/bundle-files.js +11 -14
  35. package/dist/src/app/markdown.js +18 -6
  36. package/dist/src/app/source.d.ts +0 -4
  37. package/dist/src/app/source.js +18 -14
  38. package/dist/src/cli/arguments.d.ts +2 -8
  39. package/dist/src/cli/arguments.js +67 -80
  40. package/dist/src/cli/commands.d.ts +42 -0
  41. package/dist/src/cli/commands.js +795 -0
  42. package/dist/src/cli/config.js +12 -9
  43. package/dist/src/cli/digest.d.ts +1 -1
  44. package/dist/src/cli/digest.js +38 -16
  45. package/dist/src/cli/import.js +25 -4
  46. package/dist/src/cli/indexing.d.ts +2 -0
  47. package/dist/src/cli/indexing.js +26 -9
  48. package/dist/src/cli/main.js +47 -386
  49. package/dist/src/cli/portability.d.ts +0 -2
  50. package/dist/src/cli/portability.js +13 -8
  51. package/dist/src/cli/queue.js +12 -8
  52. package/dist/src/cli/runtimes.d.ts +1 -3
  53. package/dist/src/cli/runtimes.js +13 -6
  54. package/dist/src/cli/terminal.js +1 -1
  55. package/dist/src/cli/worker.d.ts +7 -0
  56. package/dist/src/cli/worker.js +54 -15
  57. package/dist/src/derivations/kinds.d.ts +34 -0
  58. package/dist/src/derivations/kinds.js +14 -0
  59. package/dist/src/derivations/types.d.ts +48 -0
  60. package/dist/src/derivations/types.js +1 -0
  61. package/dist/src/digest/derivation.d.ts +25 -0
  62. package/dist/src/digest/derivation.js +124 -0
  63. package/dist/src/digest/editorial.d.ts +9 -46
  64. package/dist/src/digest/editorial.js +57 -49
  65. package/dist/src/digest/endpoint.js +63 -27
  66. package/dist/src/digest/pipeline.d.ts +49 -28
  67. package/dist/src/digest/pipeline.js +369 -102
  68. package/dist/src/digest/schema.d.ts +11 -25
  69. package/dist/src/digest/schema.js +86 -18
  70. package/dist/src/errors.d.ts +46 -0
  71. package/dist/src/errors.js +117 -0
  72. package/dist/src/identity/query-policy.d.ts +0 -5
  73. package/dist/src/identity/query-policy.js +61 -9
  74. package/dist/src/identity/uri.d.ts +6 -5
  75. package/dist/src/identity/uri.js +37 -18
  76. package/dist/src/identity/visibility.d.ts +11 -1
  77. package/dist/src/identity/visibility.js +81 -17
  78. package/dist/src/importers/apply.js +104 -36
  79. package/dist/src/importers/custody.js +27 -9
  80. package/dist/src/importers/inspect.js +125 -38
  81. package/dist/src/importers/jungle-digest.d.ts +3 -6
  82. package/dist/src/importers/jungle-digest.js +196 -69
  83. package/dist/src/importers/jungle-store.d.ts +19 -6
  84. package/dist/src/importers/jungle-store.js +124 -28
  85. package/dist/src/importers/jungle.d.ts +14 -14
  86. package/dist/src/importers/jungle.js +142 -31
  87. package/dist/src/importers/manifest.js +57 -21
  88. package/dist/src/importers/verify-helper.js +18 -4
  89. package/dist/src/indexing/image.js +29 -10
  90. package/dist/src/indexing/index.d.ts +37 -6
  91. package/dist/src/indexing/index.js +360 -124
  92. package/dist/src/indexing/jobs.js +156 -47
  93. package/dist/src/indexing/prune-run.d.ts +2 -0
  94. package/dist/src/indexing/prune-run.js +69 -26
  95. package/dist/src/indexing/prune.d.ts +12 -3
  96. package/dist/src/indexing/prune.js +85 -35
  97. package/dist/src/indexing/runner.d.ts +2 -1
  98. package/dist/src/indexing/runner.js +83 -40
  99. package/dist/src/indexing/upgrade.d.ts +9 -0
  100. package/dist/src/indexing/upgrade.js +79 -0
  101. package/dist/src/inference/asr/bridge.js +6 -2
  102. package/dist/src/inference/asr/protocol.js +109 -19
  103. package/dist/src/inference/asr/runtime.js +58 -21
  104. package/dist/src/inference/asr/windows.js +9 -5
  105. package/dist/src/inference/embedding/index.d.ts +2 -6
  106. package/dist/src/inference/embedding/index.js +89 -29
  107. package/dist/src/ingest/attempt.d.ts +29 -0
  108. package/dist/src/ingest/attempt.js +55 -0
  109. package/dist/src/media/capture-helper.js +7 -2
  110. package/dist/src/media/capture.d.ts +2 -1
  111. package/dist/src/media/capture.js +124 -43
  112. package/dist/src/media/handler.d.ts +4 -2
  113. package/dist/src/media/handler.js +88 -70
  114. package/dist/src/media/transcribe.js +63 -21
  115. package/dist/src/metadata/article.js +49 -17
  116. package/dist/src/models/artifacts.d.ts +10 -8
  117. package/dist/src/models/artifacts.js +237 -106
  118. package/dist/src/models/catalog.d.ts +2 -5
  119. package/dist/src/models/catalog.js +94 -25
  120. package/dist/src/models/download.js +55 -15
  121. package/dist/src/models/runtime.d.ts +0 -2
  122. package/dist/src/models/runtime.js +69 -33
  123. package/dist/src/pdf/capture-helper.js +3 -1
  124. package/dist/src/pdf/capture.d.ts +2 -1
  125. package/dist/src/pdf/capture.js +95 -27
  126. package/dist/src/pdf/errors.js +12 -1
  127. package/dist/src/pdf/handler.d.ts +2 -1
  128. package/dist/src/pdf/handler.js +70 -71
  129. package/dist/src/pdf/index.d.ts +1 -3
  130. package/dist/src/pdf/index.js +0 -2
  131. package/dist/src/pdf/input.js +12 -7
  132. package/dist/src/pdf/normalize.js +80 -21
  133. package/dist/src/pdf/source-identity.js +3 -2
  134. package/dist/src/pdf/source.js +12 -8
  135. package/dist/src/pdf/verify-helper.js +15 -3
  136. package/dist/src/portability/backup.d.ts +2 -9
  137. package/dist/src/portability/backup.js +106 -22
  138. package/dist/src/portability/database.js +51 -17
  139. package/dist/src/portability/export.js +35 -9
  140. package/dist/src/portability/files.d.ts +1 -4
  141. package/dist/src/portability/files.js +127 -67
  142. package/dist/src/portability/gc.js +65 -22
  143. package/dist/src/publish/component.js +1 -1
  144. package/dist/src/publish/feed.d.ts +2 -2
  145. package/dist/src/publish/feed.js +75 -29
  146. package/dist/src/publish/identity.d.ts +5 -1
  147. package/dist/src/publish/identity.js +31 -11
  148. package/dist/src/publish/index.d.ts +2 -3
  149. package/dist/src/publish/index.js +34 -21
  150. package/dist/src/publish/projection.d.ts +2 -9
  151. package/dist/src/publish/projection.js +21 -14
  152. package/dist/src/search/extension.js +3 -1
  153. package/dist/src/search/hybrid.d.ts +3 -8
  154. package/dist/src/search/hybrid.js +45 -11
  155. package/dist/src/search/lexical.js +18 -4
  156. package/dist/src/search/query.js +56 -25
  157. package/dist/src/search/vector.js +37 -10
  158. package/dist/src/service/launch-agent.d.ts +5 -3
  159. package/dist/src/service/launch-agent.js +272 -83
  160. package/dist/src/service/lifetime.js +47 -14
  161. package/dist/src/service/loop.d.ts +3 -3
  162. package/dist/src/service/loop.js +47 -19
  163. package/dist/src/store/annotations.d.ts +32 -0
  164. package/dist/src/store/annotations.js +44 -0
  165. package/dist/src/store/backups.d.ts +18 -0
  166. package/dist/src/store/backups.js +110 -0
  167. package/dist/src/store/blobs.d.ts +24 -0
  168. package/dist/src/store/blobs.js +139 -0
  169. package/dist/src/store/changes.d.ts +5 -12
  170. package/dist/src/store/changes.js +108 -56
  171. package/dist/src/store/context.d.ts +15 -0
  172. package/dist/src/store/context.js +19 -0
  173. package/dist/src/store/derivations.d.ts +42 -0
  174. package/dist/src/store/derivations.js +104 -0
  175. package/dist/src/store/files.d.ts +0 -2
  176. package/dist/src/store/files.js +19 -24
  177. package/dist/src/store/ids.d.ts +30 -0
  178. package/dist/src/store/ids.js +34 -0
  179. package/dist/src/store/imports.d.ts +26 -0
  180. package/dist/src/store/imports.js +57 -0
  181. package/dist/src/store/index.d.ts +3 -186
  182. package/dist/src/store/index.js +2 -809
  183. package/dist/src/store/lease.d.ts +20 -0
  184. package/dist/src/store/lease.js +62 -0
  185. package/dist/src/store/library.d.ts +75 -0
  186. package/dist/src/store/library.js +174 -0
  187. package/dist/src/store/open.d.ts +7 -0
  188. package/dist/src/store/open.js +183 -0
  189. package/dist/src/store/queue.d.ts +65 -0
  190. package/dist/src/store/queue.js +327 -0
  191. package/dist/src/store/revisions.d.ts +12 -0
  192. package/dist/src/store/revisions.js +189 -0
  193. package/dist/src/store/search-index.d.ts +63 -0
  194. package/dist/src/store/search-index.js +190 -0
  195. package/dist/src/store/sync-ready.d.ts +6 -0
  196. package/dist/src/store/sync-ready.js +121 -0
  197. package/dist/src/store/types.d.ts +1 -5
  198. package/dist/src/store/types.js +1 -8
  199. package/dist/src/util/canonical.d.ts +2 -0
  200. package/dist/src/util/canonical.js +17 -0
  201. package/dist/src/util/exact.d.ts +2 -0
  202. package/dist/src/util/exact.js +9 -0
  203. package/dist/src/util/fsync.d.ts +4 -0
  204. package/dist/src/util/fsync.js +22 -0
  205. package/dist/src/util/hash.d.ts +4 -0
  206. package/dist/src/util/hash.js +8 -0
  207. package/dist/src/worker/basic.d.ts +0 -1
  208. package/dist/src/worker/basic.js +23 -7
  209. package/dist/src/worker/custody.d.ts +4 -1
  210. package/dist/src/worker/custody.js +111 -38
  211. package/dist/src/worker/engine.d.ts +2 -1
  212. package/dist/src/worker/engine.js +42 -20
  213. package/dist/src/worker/identity.js +13 -5
  214. package/dist/src/worker/journal.d.ts +1 -1
  215. package/dist/src/worker/journal.js +54 -24
  216. package/dist/src/worker/lock.d.ts +18 -0
  217. package/dist/src/worker/lock.js +196 -0
  218. package/dist/src/worker/runtime-gc.d.ts +4 -3
  219. package/dist/src/worker/runtime-gc.js +68 -41
  220. package/dist/src/worker/supervisor-child.js +19 -6
  221. package/dist/src/worker/supervisor.js +41 -8
  222. package/native/runtime-manifest.json +4 -4
  223. package/package.json +10 -2
  224. package/dist/src/digest/store.d.ts +0 -57
  225. package/dist/src/digest/store.js +0 -79
  226. package/dist/src/store/foundation.d.ts +0 -7
  227. package/dist/src/store/foundation.js +0 -7
@@ -26,14 +26,10 @@ export interface EmbeddingRuntime {
26
26
  model: string;
27
27
  library: string;
28
28
  }
29
- export declare class EmbeddingError extends Error {
30
- code: 'integrity' | 'unsupported' | 'limit-exceeded' | 'cancelled' | 'unavailable' | 'cleanup-required';
31
- constructor(code: 'integrity' | 'unsupported' | 'limit-exceeded' | 'cancelled' | 'unavailable' | 'cleanup-required');
32
- }
33
29
  export declare function spaceFingerprint(identity: Record<string, string | number>): string;
34
30
  export declare function embeddingFingerprint(helperSha256: string): string;
35
31
  /** Stored index spaces over the fixed 768-dim helper output. `f32-768` is the legacy identity and keeps its original fingerprint so existing generations stay readable; `int8-768` (default) stores symmetric per-vector max-abs int8 codes of the full vector; `int8-512` also keeps only the renormalized Matryoshka prefix. Quantization alone was near-lossless on real queries while the 512 prefix moved chunk top-1 on a quarter of them (docs/decisions/storage-diet.md), so the default keeps all 768 dimensions for 256 more bytes per row. */
36
- export declare const indexEncodings: {
32
+ declare const indexEncodings: {
37
33
  readonly 'f32-768': {};
38
34
  readonly 'int8-768': {
39
35
  readonly quantization: "int8-symmetric-maxabs-v1";
@@ -45,7 +41,6 @@ export declare const indexEncodings: {
45
41
  };
46
42
  };
47
43
  export type IndexEncoding = keyof typeof indexEncodings;
48
- export declare const defaultIndexEncoding: IndexEncoding;
49
44
  export interface IndexSpace {
50
45
  encoding: IndexEncoding;
51
46
  identity: Record<string, string | number>;
@@ -73,3 +68,4 @@ export declare function embedBatch(inputs: readonly EmbeddingInput[], options: {
73
68
  inputHashes: string[];
74
69
  vectors: number[][];
75
70
  }>;
71
+ export {};
@@ -1,35 +1,75 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { sha256 as hash } from '../../util/hash.js';
2
3
  import { constants } from 'node:fs';
3
4
  import { open, mkdtemp, chmod, writeFile, rm, realpath } from 'node:fs/promises';
4
5
  import { dirname, join, isAbsolute } from 'node:path';
5
6
  import { runOwned } from '../../acquisition/process.js';
6
- export const embeddingIdentity = { modelSha256: '92dcbea108899e5d6e30d919b0744f90d9967e80c67a4ab5503ac16d54f62eb0', librarySha256: '9526db9ee35e7eb4a5a58ac1e53330032f22a13890bd4ee01818fb380a57a362', runtime: 'LiteRT-LM@0.18.0', dimensions: 768, normalization: 'unit-l2', preprocessing: 'sips-max512-png-litert-v1', prompt: 'search-result-query-title-none-v1', protocol: 1, backend: 'cpu-2threads-f32', textBudget: 'utf8-1016-v1' };
7
+ import { SpongeError } from '../../errors.js';
8
+ export const embeddingIdentity = {
9
+ modelSha256: '92dcbea108899e5d6e30d919b0744f90d9967e80c67a4ab5503ac16d54f62eb0',
10
+ librarySha256: '9526db9ee35e7eb4a5a58ac1e53330032f22a13890bd4ee01818fb380a57a362',
11
+ runtime: 'LiteRT-LM@0.18.0',
12
+ dimensions: 768,
13
+ normalization: 'unit-l2',
14
+ preprocessing: 'sips-max512-png-litert-v1',
15
+ prompt: 'search-result-query-title-none-v1',
16
+ protocol: 1,
17
+ backend: 'cpu-2threads-f32',
18
+ textBudget: 'utf8-1016-v1',
19
+ };
7
20
  /** The model's largest text signature is 1024 tokens; byte-fallback tokenization never yields more tokens than UTF-8 bytes. */
8
21
  export const embeddingTextBytes = 1016;
9
- export class EmbeddingError extends Error {
10
- code;
11
- constructor(code) {
12
- super(code);
13
- this.code = code;
14
- }
22
+ const fail = (code = 'integrity') => {
23
+ throw new SpongeError(code);
24
+ };
25
+ export function spaceFingerprint(identity) {
26
+ const keys = Object.keys(identity).sort();
27
+ if (!keys.length ||
28
+ keys.some((k) => !['string', 'number'].includes(typeof identity[k]) ||
29
+ (typeof identity[k] === 'number' && !Number.isFinite(identity[k]))))
30
+ fail();
31
+ return hash(JSON.stringify(Object.fromEntries(keys.map((k) => [k, identity[k]]))));
32
+ }
33
+ export function embeddingFingerprint(helperSha256) {
34
+ if (!/^[a-f0-9]{64}$/.test(helperSha256))
35
+ fail();
36
+ return spaceFingerprint({ ...embeddingIdentity, helperSha256 });
15
37
  }
16
- const fail = (code = 'integrity') => { throw new EmbeddingError(code); };
17
- const hash = (b) => createHash('sha256').update(b).digest('hex');
18
- export function spaceFingerprint(identity) { const keys = Object.keys(identity).sort(); if (!keys.length || keys.some(k => !['string', 'number'].includes(typeof identity[k]) || (typeof identity[k] === 'number' && !Number.isFinite(identity[k]))))
19
- fail(); return hash(JSON.stringify(Object.fromEntries(keys.map(k => [k, identity[k]])))); }
20
- export function embeddingFingerprint(helperSha256) { if (!/^[a-f0-9]{64}$/.test(helperSha256))
21
- fail(); return spaceFingerprint({ ...embeddingIdentity, helperSha256 }); }
22
38
  /** Stored index spaces over the fixed 768-dim helper output. `f32-768` is the legacy identity and keeps its original fingerprint so existing generations stay readable; `int8-768` (default) stores symmetric per-vector max-abs int8 codes of the full vector; `int8-512` also keeps only the renormalized Matryoshka prefix. Quantization alone was near-lossless on real queries while the 512 prefix moved chunk top-1 on a quarter of them (docs/decisions/storage-diet.md), so the default keeps all 768 dimensions for 256 more bytes per row. */
23
- export const indexEncodings = { 'f32-768': {}, 'int8-768': { quantization: 'int8-symmetric-maxabs-v1' }, 'int8-512': { dimensions: 512, truncation: 'matryoshka-prefix-renormalized-v1', quantization: 'int8-symmetric-maxabs-v1' } };
24
- export const defaultIndexEncoding = 'int8-768';
25
- export function indexSpace(helperSha256, encoding = defaultIndexEncoding) { if (!/^[a-f0-9]{64}$/.test(helperSha256) || !Object.hasOwn(indexEncodings, encoding))
26
- fail(); const identity = { ...embeddingIdentity, helperSha256, ...indexEncodings[encoding] }; return { encoding, identity, fingerprint: spaceFingerprint(identity), dimensions: Number(identity.dimensions) }; }
39
+ const indexEncodings = {
40
+ 'f32-768': {},
41
+ 'int8-768': { quantization: 'int8-symmetric-maxabs-v1' },
42
+ 'int8-512': {
43
+ dimensions: 512,
44
+ truncation: 'matryoshka-prefix-renormalized-v1',
45
+ quantization: 'int8-symmetric-maxabs-v1',
46
+ },
47
+ };
48
+ const defaultIndexEncoding = 'int8-768';
49
+ export function indexSpace(helperSha256, encoding = defaultIndexEncoding) {
50
+ if (!/^[a-f0-9]{64}$/.test(helperSha256) || !Object.hasOwn(indexEncodings, encoding))
51
+ fail();
52
+ const identity = { ...embeddingIdentity, helperSha256, ...indexEncodings[encoding] };
53
+ return { encoding, identity, fingerprint: spaceFingerprint(identity), dimensions: Number(identity.dimensions) };
54
+ }
27
55
  /** The stored space for a generation fingerprint built from this helper, if any. */
28
- export function resolveIndexSpace(helperSha256, fingerprint) { return Object.keys(indexEncodings).map(e => indexSpace(helperSha256, e)).find(s => s.fingerprint === fingerprint); }
56
+ export function resolveIndexSpace(helperSha256, fingerprint) {
57
+ return Object.keys(indexEncodings)
58
+ .map((e) => indexSpace(helperSha256, e))
59
+ .find((s) => s.fingerprint === fingerprint);
60
+ }
29
61
  /** Matryoshka prefix truncation of a unit helper vector, renormalized to unit L2. */
30
- export function projectVector(vector, dimensions) { if (!Number.isSafeInteger(dimensions) || dimensions < 1 || dimensions > vector.length || vector.some(n => !Number.isFinite(n)))
31
- fail(); const prefix = vector.slice(0, dimensions), norm = Math.hypot(...prefix); if (!(norm > 0))
32
- fail(); return dimensions === vector.length ? [...vector] : prefix.map(n => n / norm); }
62
+ export function projectVector(vector, dimensions) {
63
+ if (!Number.isSafeInteger(dimensions) ||
64
+ dimensions < 1 ||
65
+ dimensions > vector.length ||
66
+ vector.some((n) => !Number.isFinite(n)))
67
+ fail();
68
+ const prefix = vector.slice(0, dimensions), norm = Math.hypot(...prefix);
69
+ if (!(norm > 0))
70
+ fail();
71
+ return dimensions === vector.length ? [...vector] : prefix.map((n) => n / norm);
72
+ }
33
73
  /** Versioned length-delimited bytes. No user content or image pathname enters argv. */
34
74
  export function encodeEmbeddingRequest(inputs) {
35
75
  if (!inputs.length || inputs.length > 16)
@@ -42,7 +82,9 @@ export function encodeEmbeddingRequest(inputs) {
42
82
  kind = 2;
43
83
  if (payload.length < 33 || payload.length > 2 * 1024 * 1024)
44
84
  fail('limit-exceeded');
45
- if (!payload.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])) || payload.toString('ascii', 12, 16) !== 'IHDR' || payload.readUInt32BE(8) !== 13)
85
+ if (!payload.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])) ||
86
+ payload.toString('ascii', 12, 16) !== 'IHDR' ||
87
+ payload.readUInt32BE(8) !== 13)
46
88
  fail('unsupported');
47
89
  const w = payload.readUInt32BE(16), h = payload.readUInt32BE(20);
48
90
  if (!w || !h || w * h > 262144)
@@ -79,9 +121,14 @@ export function parseEmbeddingOutput(stdout, count) {
79
121
  const p = parsed;
80
122
  if (!p || p.protocol !== 1 || !Array.isArray(p.vectors) || p.vectors.length !== count)
81
123
  fail();
82
- return p.vectors.map((v) => { if (!Array.isArray(v) || v.length !== 768 || v.some(n => typeof n !== 'number' || !Number.isFinite(n)))
83
- fail(); const vector = v; if (Math.abs(Math.hypot(...vector) - 1) > .001)
84
- fail(); return vector; });
124
+ return p.vectors.map((v) => {
125
+ if (!Array.isArray(v) || v.length !== 768 || v.some((n) => typeof n !== 'number' || !Number.isFinite(n)))
126
+ fail();
127
+ const vector = v;
128
+ if (Math.abs(Math.hypot(...vector) - 1) > 0.001)
129
+ fail();
130
+ return vector;
131
+ });
85
132
  }
86
133
  async function verify(path, expected) {
87
134
  if (!isAbsolute(path) || !/^[a-f0-9]{64}$/.test(expected))
@@ -108,8 +155,12 @@ export async function embedBatch(inputs, options) {
108
155
  fail('cancelled');
109
156
  if (process.platform !== 'darwin' || process.arch !== 'arm64')
110
157
  fail('unsupported');
111
- await Promise.all([verify(r.helper, r.helperSha256), verify(r.model, embeddingIdentity.modelSha256), verify(r.library, embeddingIdentity.librarySha256)]);
112
- if (await realpath(r.library) !== join(await realpath(dirname(r.helper)), 'libCLiteRTLM_mac.dylib'))
158
+ await Promise.all([
159
+ verify(r.helper, r.helperSha256),
160
+ verify(r.model, embeddingIdentity.modelSha256),
161
+ verify(r.library, embeddingIdentity.librarySha256),
162
+ ]);
163
+ if ((await realpath(r.library)) !== join(await realpath(dirname(r.helper)), 'libCLiteRTLM_mac.dylib'))
113
164
  fail();
114
165
  const scratch = await mkdtemp(join(options.scratchRoot, 'embedding-'));
115
166
  await chmod(scratch, 0o700);
@@ -118,7 +169,12 @@ export async function embedBatch(inputs, options) {
118
169
  const path = join(scratch, 'request.bin');
119
170
  await writeFile(path, request.bytes, { mode: 0o600, flag: 'wx' });
120
171
  settled = false;
121
- const result = await (options.runProcess ?? runOwned)('/usr/bin/sandbox-exec', ['-p', '(version 1) (allow default) (deny network*)', r.helper, r.model, path], { env: { PATH: '/usr/bin:/bin', HOME: scratch, TMPDIR: scratch, LANG: 'C' }, signal: options.signal, deadlineMs: 120000, maxOutputBytes: 512 * 1024 });
172
+ const result = await (options.runProcess ?? runOwned)('/usr/bin/sandbox-exec', ['-p', '(version 1) (allow default) (deny network*)', r.helper, r.model, path], {
173
+ env: { PATH: '/usr/bin:/bin', HOME: scratch, TMPDIR: scratch, LANG: 'C' },
174
+ signal: options.signal,
175
+ deadlineMs: 120000,
176
+ maxOutputBytes: 512 * 1024,
177
+ });
122
178
  settled = result.cleanup === 'settled';
123
179
  if (!settled)
124
180
  fail('cleanup-required');
@@ -126,7 +182,11 @@ export async function embedBatch(inputs, options) {
126
182
  fail(result.stopped);
127
183
  if (result.code !== 0)
128
184
  fail('unavailable');
129
- return { fingerprint, inputHashes: request.inputHashes, vectors: parseEmbeddingOutput(result.stdout, inputs.length) };
185
+ return {
186
+ fingerprint,
187
+ inputHashes: request.inputHashes,
188
+ vectors: parseEmbeddingOutput(result.stdout, inputs.length),
189
+ };
130
190
  }
131
191
  finally {
132
192
  if (settled)
@@ -0,0 +1,29 @@
1
+ import type { ErrorCode, ErrorEnvelope } from '../../contracts/index.js';
2
+ import type { LibraryStore, AttemptToken } from '../store/index.js';
3
+ type Cleanup = 'settled' | 'pending';
4
+ export type AttemptOutcome<T extends {
5
+ ok: true;
6
+ }> = (T | ErrorEnvelope) & {
7
+ cleanup: Cleanup;
8
+ };
9
+ /** A body's view of its attempt: the cleanup verdict of its owned children, and the one capture it holds. */
10
+ export interface Attempt {
11
+ cleanup: Cleanup;
12
+ hold(capture: {
13
+ dispose(): Promise<void>;
14
+ }): void;
15
+ }
16
+ /** Maps a thrown value to one of `codes` by its public code; anything else is unclassified. */
17
+ export declare function classifyErrors(codes: readonly ErrorCode[]): (error: unknown) => ErrorCode;
18
+ /**
19
+ * One ingest attempt. The body captures, stores blobs and commits; it returns ok only after revisions.commit.
20
+ * Afterwards, in order: dispose the held capture (only while every child is settled: an uncertain process may
21
+ * still read its bytes), fail an uncommitted attempt, settle storage staging when clean. Any bookkeeping failure
22
+ * leaves ownership pending, reported as cleanup-required.
23
+ */
24
+ export declare function runAttempt<T extends {
25
+ ok: true;
26
+ }>(store: {
27
+ queue: Pick<LibraryStore['queue'], 'fail' | 'settle'>;
28
+ }, token: AttemptToken, classify: (error: unknown, attempt: Attempt) => ErrorCode, body: (attempt: Attempt) => Promise<T | ErrorEnvelope>): Promise<AttemptOutcome<T>>;
29
+ export {};
@@ -0,0 +1,55 @@
1
+ import { failure } from '../acquisition/errors.js';
2
+ import { publicCode } from '../errors.js';
3
+ /** Maps a thrown value to one of `codes` by its public code; anything else is unclassified. */
4
+ export function classifyErrors(codes) {
5
+ const known = new Set(codes);
6
+ return (error) => {
7
+ const code = publicCode(error);
8
+ return known.has(code) ? code : 'unclassified';
9
+ };
10
+ }
11
+ /**
12
+ * One ingest attempt. The body captures, stores blobs and commits; it returns ok only after revisions.commit.
13
+ * Afterwards, in order: dispose the held capture (only while every child is settled: an uncertain process may
14
+ * still read its bytes), fail an uncommitted attempt, settle storage staging when clean. Any bookkeeping failure
15
+ * leaves ownership pending, reported as cleanup-required.
16
+ */
17
+ export async function runAttempt(store, token, classify, body) {
18
+ let held, outcome;
19
+ const attempt = {
20
+ cleanup: 'settled',
21
+ hold: (capture) => {
22
+ held = capture;
23
+ },
24
+ };
25
+ try {
26
+ outcome = await body(attempt);
27
+ }
28
+ catch (error) {
29
+ outcome = failure(token.attemptId, classify(error, attempt), 'storage');
30
+ }
31
+ if (held && attempt.cleanup === 'settled')
32
+ try {
33
+ await held.dispose();
34
+ }
35
+ catch {
36
+ attempt.cleanup = 'pending';
37
+ }
38
+ if (!outcome.ok)
39
+ try {
40
+ store.queue.fail(token, outcome.code, { retryable: outcome.retryable, retryAfterMs: outcome.retryAfterMs });
41
+ }
42
+ catch {
43
+ attempt.cleanup = 'pending';
44
+ }
45
+ if (attempt.cleanup === 'settled')
46
+ try {
47
+ store.queue.settle(token);
48
+ }
49
+ catch {
50
+ attempt.cleanup = 'pending';
51
+ }
52
+ return attempt.cleanup === 'pending'
53
+ ? { ...failure(token.attemptId, 'cleanup-required', 'cleanup'), cleanup: 'pending' }
54
+ : { ...outcome, cleanup: 'settled' };
55
+ }
@@ -1,13 +1,18 @@
1
1
  /** Public Ghostget SDK bridge; its complete inherited group belongs to the caller. */
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { processIdentity } from '../worker/identity.js';
4
+ import { SpongeError } from '../errors.js';
4
5
  try {
5
6
  const config = JSON.parse(await readFile(process.argv[2], 'utf8'));
6
7
  const sdk = await import(import.meta.resolve('@hraness/ghostget/media-acquisition', `${config.runtimeRoot}/package.json`));
7
8
  const identity = await processIdentity(process.pid);
8
9
  if (!identity)
9
- throw Error('identity');
10
- const result = await sdk.acquireEphemeralMedia({ ...config.request, processGroup: 'inherit', supervisor: { requestId: config.requestId, processGroupId: process.pid, processStartIdentity: identity.started } });
10
+ throw new SpongeError('unavailable');
11
+ const result = await sdk.acquireEphemeralMedia({
12
+ ...config.request,
13
+ processGroup: 'inherit',
14
+ supervisor: { requestId: config.requestId, processGroupId: process.pid, processStartIdentity: identity.started },
15
+ });
11
16
  process.stdout.write(JSON.stringify(result));
12
17
  }
13
18
  catch {
@@ -15,7 +15,7 @@ export interface MediaCaptureRequest {
15
15
  signal?: AbortSignal;
16
16
  runProcess?: typeof runOwned;
17
17
  }
18
- export interface CapturedMedia {
18
+ interface CapturedMedia {
19
19
  ok: true;
20
20
  root: string;
21
21
  audio?: AudioInput;
@@ -30,3 +30,4 @@ export interface CapturedMedia {
30
30
  export type MediaCaptureOutcome = CapturedMedia | ErrorEnvelope;
31
31
  /** All ephemeral bytes remain below one capture owner; deletion requires a settled process tree. */
32
32
  export declare function captureMedia(request: MediaCaptureRequest): Promise<MediaCaptureOutcome>;
33
+ export {};
@@ -4,13 +4,14 @@ import { join, relative, isAbsolute } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { homedir } from 'node:os';
6
6
  import { createHash } from 'node:crypto';
7
+ import { sha256 as hash } from '../util/hash.js';
7
8
  import { limits } from '../../contracts/index.js';
8
9
  import { failure } from '../acquisition/errors.js';
9
- import { inspectCaptureRuntime, CaptureSetupError } from '../acquisition/runtime.js';
10
+ import { inspectCaptureRuntime } from '../acquisition/runtime.js';
10
11
  import { privateRoot, physicalBytes, readConfined } from '../acquisition/files.js';
11
12
  import { runOwned, isolatedEnvironment } from '../acquisition/process.js';
12
- import { settleCaptureRoot, CaptureCleanupError } from '../acquisition/cleanup.js';
13
- const hash = (value) => createHash('sha256').update(value).digest('hex');
13
+ import { settleCaptureRoot } from '../acquisition/cleanup.js';
14
+ import { SpongeError, publicCode, codeOf } from '../errors.js';
14
15
  /** All ephemeral bytes remain below one capture owner; deletion requires a settled process tree. */
15
16
  export async function captureMedia(request) {
16
17
  const { requestId, signal } = request;
@@ -39,7 +40,7 @@ export async function captureMedia(request) {
39
40
  return failure(requestId, 'limit-exceeded');
40
41
  }
41
42
  catch (error) {
42
- return failure(requestId, error instanceof CaptureSetupError ? error.code : 'invalid-input');
43
+ return failure(requestId, publicCode(error, 'invalid-input'));
43
44
  }
44
45
  let selectedState;
45
46
  if (request.connection?.kind === 'account') {
@@ -58,25 +59,44 @@ export async function captureMedia(request) {
58
59
  signal?.addEventListener('abort', abort, { once: true });
59
60
  if (signal?.aborted)
60
61
  abort();
61
- const monitor = setInterval(async () => { if (checking)
62
- return; checking = true; try {
63
- if (await physicalBytes(root) > limits.stagingBytes) {
62
+ const monitor = setInterval(async () => {
63
+ if (checking)
64
+ return;
65
+ checking = true;
66
+ try {
67
+ if ((await physicalBytes(root)) > limits.stagingBytes) {
68
+ quota = true;
69
+ controller.abort();
70
+ }
71
+ }
72
+ catch {
64
73
  quota = true;
65
74
  controller.abort();
66
75
  }
67
- }
68
- catch {
69
- quota = true;
70
- controller.abort();
71
- }
72
- finally {
73
- checking = false;
74
- } }, 100);
76
+ finally {
77
+ checking = false;
78
+ }
79
+ }, 100);
75
80
  try {
76
81
  await mkdir(home, { mode: 0o700 });
77
- await writeFile(join(root, 'ownership.json'), JSON.stringify({ schemaVersion: 1, requestId, state: 'acquiring' }), { mode: 0o600 });
82
+ await writeFile(join(root, 'ownership.json'), JSON.stringify({ schemaVersion: 1, requestId, state: 'acquiring' }), {
83
+ mode: 0o600,
84
+ });
78
85
  const requestFile = join(root, 'request.json');
79
- await writeFile(requestFile, JSON.stringify({ runtimeRoot: runtime.root, requestId, request: { url: request.sourceUrl, stagingParent: root, accountId: request.connection?.kind === 'account' ? request.connection.accountId : undefined, language: request.language ?? 'en', preferAudio: request.forceLocal === true, maxBytes: 512 * 1024 ** 2, maxDurationSeconds: 3600, timeoutMs: limits.captureDeadlineMs } }), { mode: 0o600, flag: 'wx' });
86
+ await writeFile(requestFile, JSON.stringify({
87
+ runtimeRoot: runtime.root,
88
+ requestId,
89
+ request: {
90
+ url: request.sourceUrl,
91
+ stagingParent: root,
92
+ accountId: request.connection?.kind === 'account' ? request.connection.accountId : undefined,
93
+ language: request.language ?? 'en',
94
+ preferAudio: request.forceLocal === true,
95
+ maxBytes: 512 * 1024 ** 2,
96
+ maxDurationSeconds: 3600,
97
+ timeoutMs: limits.captureDeadlineMs,
98
+ },
99
+ }), { mode: 0o600, flag: 'wx' });
80
100
  const entry = fileURLToPath(new URL(import.meta.url.endsWith('.ts') ? './capture-helper.ts' : './capture-helper.js', import.meta.url));
81
101
  const environment = isolatedEnvironment(home, home, selectedState ?? join(home, 'state'));
82
102
  environment.XDG_CACHE_HOME = home;
@@ -85,7 +105,13 @@ export async function captureMedia(request) {
85
105
  if (selectedState)
86
106
  environment.HOME = homedir();
87
107
  cleanup = 'pending';
88
- const result = await (request.runProcess ?? runOwned)(runtime.bun, [entry, requestFile], { cwd: root, env: environment, signal: controller.signal, deadlineMs: limits.captureDeadlineMs, maxOutputBytes: limits.metadataBytes });
108
+ const result = await (request.runProcess ?? runOwned)(runtime.bun, [entry, requestFile], {
109
+ cwd: root,
110
+ env: environment,
111
+ signal: controller.signal,
112
+ deadlineMs: limits.captureDeadlineMs,
113
+ maxOutputBytes: limits.metadataBytes,
114
+ });
89
115
  cleanup = result.cleanup;
90
116
  if (cleanup === 'pending')
91
117
  return failure(requestId, 'cleanup-required', 'cleanup');
@@ -97,42 +123,80 @@ export async function captureMedia(request) {
97
123
  if (parsed.ok !== true) {
98
124
  if (parsed.code === 'cleanup-required')
99
125
  cleanup = 'pending';
100
- const mapping = { 'permission-or-provider-failure': 'grant-permission', timeout: 'unavailable', 'cleanup-required': 'cleanup-required', 'invalid-input': 'invalid-input', unsupported: 'unsupported', unavailable: 'unavailable', 'limit-exceeded': 'limit-exceeded', cancelled: 'cancelled', integrity: 'integrity' };
126
+ const mapping = {
127
+ 'permission-or-provider-failure': 'grant-permission',
128
+ timeout: 'unavailable',
129
+ 'cleanup-required': 'cleanup-required',
130
+ 'invalid-input': 'invalid-input',
131
+ unsupported: 'unsupported',
132
+ unavailable: 'unavailable',
133
+ 'limit-exceeded': 'limit-exceeded',
134
+ cancelled: 'cancelled',
135
+ integrity: 'integrity',
136
+ };
101
137
  return failure(requestId, mapping[parsed.code] ?? 'unclassified');
102
138
  }
103
139
  const manifest = parsed.manifest, mediaRoot = parsed.receipt?.root;
104
- if (result.code !== 0 || typeof mediaRoot !== 'string' || !mediaRoot.startsWith(root + '/media-') || await realpath(mediaRoot) !== mediaRoot || manifest?.schemaVersion !== 1 || manifest.kind !== 'ephemeral-media' || manifest.retainedSourceArchive !== false || manifest.source?.requestedUrlSha256 !== hash(url.href) || !Number.isFinite(manifest.source.durationSeconds) || manifest.source.durationSeconds <= 0 || manifest.source.durationSeconds > 3600 || !Array.isArray(manifest.files) || manifest.files.length > 3)
105
- throw Error('integrity');
106
- const captured = { ok: true, root: mediaRoot, manifestSha256: hash(JSON.stringify(manifest)), warnings: Array.isArray(manifest.warnings) ? manifest.warnings.map(() => 'partial') : [], dispose: async () => { if (await settleCaptureRoot(root, requestId) === 'pending')
107
- throw new CaptureCleanupError(); } };
140
+ if (result.code !== 0 ||
141
+ typeof mediaRoot !== 'string' ||
142
+ !mediaRoot.startsWith(root + '/media-') ||
143
+ (await realpath(mediaRoot)) !== mediaRoot ||
144
+ manifest?.schemaVersion !== 1 ||
145
+ manifest.kind !== 'ephemeral-media' ||
146
+ manifest.retainedSourceArchive !== false ||
147
+ manifest.source?.requestedUrlSha256 !== hash(url.href) ||
148
+ !Number.isFinite(manifest.source.durationSeconds) ||
149
+ manifest.source.durationSeconds <= 0 ||
150
+ manifest.source.durationSeconds > 3600 ||
151
+ !Array.isArray(manifest.files) ||
152
+ manifest.files.length > 3)
153
+ throw new SpongeError('integrity');
154
+ const captured = {
155
+ ok: true,
156
+ root: mediaRoot,
157
+ manifestSha256: hash(JSON.stringify(manifest)),
158
+ warnings: Array.isArray(manifest.warnings) ? manifest.warnings.map(() => 'partial') : [],
159
+ dispose: async () => {
160
+ if ((await settleCaptureRoot(root, requestId)) === 'pending')
161
+ throw new SpongeError('cleanup-required');
162
+ },
163
+ };
108
164
  for (const field of ['title', 'author']) {
109
165
  const value = manifest.source[field];
110
166
  if (value !== undefined) {
111
167
  if (typeof value !== 'string' || Buffer.byteLength(value) > 16384)
112
- throw Error('integrity');
168
+ throw new SpongeError('integrity');
113
169
  captured[field] = value;
114
170
  }
115
171
  }
116
172
  const roles = new Set();
117
173
  for (const file of manifest.files) {
118
- if (!['audio', 'captions', 'thumbnail'].includes(file.role) || roles.has(file.role) || typeof file.path !== 'string' || !Number.isSafeInteger(file.bytes) || file.bytes < 1 || !/^[a-f0-9]{64}$/.test(file.sha256))
119
- throw Error('integrity');
174
+ if (!['audio', 'captions', 'thumbnail'].includes(file.role) ||
175
+ roles.has(file.role) ||
176
+ typeof file.path !== 'string' ||
177
+ !Number.isSafeInteger(file.bytes) ||
178
+ file.bytes < 1 ||
179
+ !/^[a-f0-9]{64}$/.test(file.sha256))
180
+ throw new SpongeError('integrity');
120
181
  roles.add(file.role);
121
182
  const relativePath = isAbsolute(file.path) ? relative(mediaRoot, file.path) : file.path;
122
183
  const maximum = file.role === 'audio' ? 512 * 1024 ** 2 : file.role === 'captions' ? 4 * 1024 ** 2 : limits.imageBytes;
123
184
  // Bounded confined byte verification before handing anything to native code.
124
185
  let data = Buffer.alloc(0);
125
186
  if (file.role === 'audio') {
126
- if (!relativePath || relativePath.split('/').some((part) => !part || part === '.' || part === '..') || relativePath.includes('\\') || file.bytes > maximum)
127
- throw Error('integrity');
187
+ if (!relativePath ||
188
+ relativePath.split('/').some((part) => !part || part === '.' || part === '..') ||
189
+ relativePath.includes('\\') ||
190
+ file.bytes > maximum)
191
+ throw new SpongeError('integrity');
128
192
  const path = join(mediaRoot, relativePath);
129
- if (await realpath(path) !== path)
130
- throw Error('integrity');
193
+ if ((await realpath(path)) !== path)
194
+ throw new SpongeError('integrity');
131
195
  const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
132
196
  try {
133
197
  const stat = await handle.stat();
134
198
  if (!stat.isFile() || stat.nlink !== 1 || stat.size !== file.bytes)
135
- throw Error('integrity');
199
+ throw new SpongeError('integrity');
136
200
  const digest = createHash('sha256'), buffer = Buffer.alloc(65536);
137
201
  let bytes = 0;
138
202
  for (;;) {
@@ -141,11 +205,11 @@ export async function captureMedia(request) {
141
205
  break;
142
206
  bytes += bytesRead;
143
207
  if (bytes > file.bytes)
144
- throw Error('limit-exceeded');
208
+ throw new SpongeError('limit-exceeded');
145
209
  digest.update(buffer.subarray(0, bytesRead));
146
210
  }
147
211
  if (bytes !== file.bytes || digest.digest('hex') !== file.sha256)
148
- throw Error('integrity');
212
+ throw new SpongeError('integrity');
149
213
  }
150
214
  finally {
151
215
  await handle.close();
@@ -154,24 +218,41 @@ export async function captureMedia(request) {
154
218
  else {
155
219
  data = await readConfined(mediaRoot, relativePath, maximum);
156
220
  if (data.length !== file.bytes || hash(data) !== file.sha256)
157
- throw Error('integrity');
221
+ throw new SpongeError('integrity');
158
222
  }
159
223
  if (file.role === 'audio')
160
- captured.audio = { path: join(mediaRoot, relativePath), sha256: file.sha256, bytes: file.bytes, durationMs: manifest.source.durationSeconds * 1000 };
224
+ captured.audio = {
225
+ path: join(mediaRoot, relativePath),
226
+ sha256: file.sha256,
227
+ bytes: file.bytes,
228
+ durationMs: manifest.source.durationSeconds * 1000,
229
+ };
161
230
  else if (file.role === 'captions') {
162
231
  if (!manifest.caption || !['manual', 'automatic'].includes(manifest.caption.source))
163
- throw Error('integrity');
164
- captured.captions = { text: new TextDecoder('utf8', { fatal: true, ignoreBOM: true }).decode(data), sha256: file.sha256, source: manifest.caption.source, language: manifest.caption.language, durationMs: manifest.source.durationSeconds * 1000 };
232
+ throw new SpongeError('integrity');
233
+ captured.captions = {
234
+ text: new TextDecoder('utf8', { fatal: true, ignoreBOM: true }).decode(data),
235
+ sha256: file.sha256,
236
+ source: manifest.caption.source,
237
+ language: manifest.caption.language,
238
+ durationMs: manifest.source.durationSeconds * 1000,
239
+ };
165
240
  }
166
241
  else {
167
- const mediaType = data[0] === 0xff && data[1] === 0xd8 ? 'image/jpeg' : data.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])) ? 'image/png' : data.toString('ascii', 0, 4) === 'RIFF' && data.toString('ascii', 8, 12) === 'WEBP' ? 'image/webp' : undefined;
242
+ const mediaType = data[0] === 0xff && data[1] === 0xd8
243
+ ? 'image/jpeg'
244
+ : data.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))
245
+ ? 'image/png'
246
+ : data.toString('ascii', 0, 4) === 'RIFF' && data.toString('ascii', 8, 12) === 'WEBP'
247
+ ? 'image/webp'
248
+ : undefined;
168
249
  if (!mediaType)
169
- throw Error('integrity');
250
+ throw new SpongeError('integrity');
170
251
  captured.thumbnail = { relativePath, bytes: file.bytes, sha256: file.sha256, mediaType };
171
252
  }
172
253
  }
173
254
  if (!captured.audio && !captured.captions)
174
- throw Error('integrity');
255
+ throw new SpongeError('integrity');
175
256
  if (controller.signal.aborted)
176
257
  return failure(requestId, quota ? 'limit-exceeded' : 'cancelled');
177
258
  keep = true;
@@ -180,14 +261,14 @@ export async function captureMedia(request) {
180
261
  catch (error) {
181
262
  if (cleanup === 'pending')
182
263
  return failure(requestId, 'cleanup-required', 'cleanup');
183
- return failure(requestId, error instanceof Error && error.message === 'limit-exceeded' ? 'limit-exceeded' : 'integrity', 'verification');
264
+ return failure(requestId, codeOf(error) === 'limit-exceeded' ? 'limit-exceeded' : 'integrity', 'verification');
184
265
  }
185
266
  finally {
186
267
  clearInterval(monitor);
187
268
  while (checking)
188
- await new Promise(r => setTimeout(r, 5));
269
+ await new Promise((r) => setTimeout(r, 5));
189
270
  signal?.removeEventListener('abort', abort);
190
- if (!keep && cleanup === 'settled' && await settleCaptureRoot(root, requestId) === 'pending')
271
+ if (!keep && cleanup === 'settled' && (await settleCaptureRoot(root, requestId)) === 'pending')
191
272
  return failure(requestId, 'cleanup-required', 'cleanup');
192
273
  }
193
274
  }
@@ -10,13 +10,15 @@ export interface MediaOptions extends Omit<MediaCaptureRequest, 'requestId' | 's
10
10
  capture?: (request: MediaCaptureRequest) => Promise<MediaCaptureOutcome>;
11
11
  transcribe?: typeof transcribeMedia;
12
12
  }
13
- export type MediaOutcome = ({
13
+ interface MediaResult {
14
14
  ok: true;
15
15
  itemId: string;
16
16
  revisionId: string;
17
17
  status: 'complete' | 'partial';
18
- } | ErrorEnvelope) & {
18
+ }
19
+ export type MediaOutcome = (MediaResult | ErrorEnvelope) & {
19
20
  cleanup: 'settled' | 'pending';
20
21
  };
21
22
  /** Only a transcript and thumbnail enter durable storage; acquisition owns all source media. */
22
23
  export declare function ingestMedia(store: LibraryStore, token: AttemptToken, options: MediaOptions): Promise<MediaOutcome>;
24
+ export {};