@hraness/sponge 0.2.5 → 0.3.0
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.
- package/README.md +23 -10
- package/bin/sponge.js +8 -7
- package/dist/contracts/index.js +24 -6
- package/dist/contracts/validate.js +106 -29
- package/dist/migrations/008.d.ts +3 -0
- package/dist/migrations/008.js +5 -0
- package/dist/migrations/009.d.ts +3 -0
- package/dist/migrations/009.js +8 -0
- package/dist/migrations/010.d.ts +3 -0
- package/dist/migrations/010.js +11 -0
- package/dist/migrations/index.d.ts +15 -0
- package/dist/migrations/index.js +39 -0
- package/dist/src/acquisition/capture.js +87 -28
- package/dist/src/acquisition/cleanup.d.ts +0 -4
- package/dist/src/acquisition/cleanup.js +3 -5
- package/dist/src/acquisition/errors.js +17 -2
- package/dist/src/acquisition/files.d.ts +0 -1
- package/dist/src/acquisition/files.js +14 -11
- package/dist/src/acquisition/index.d.ts +2 -3
- package/dist/src/acquisition/index.js +1 -2
- package/dist/src/acquisition/lock.js +1 -1
- package/dist/src/acquisition/normalize.js +57 -18
- package/dist/src/acquisition/pins.js +22 -4
- package/dist/src/acquisition/process.js +67 -29
- package/dist/src/acquisition/runtime.d.ts +5 -7
- package/dist/src/acquisition/runtime.js +129 -68
- package/dist/src/acquisition/verify-helper.js +10 -3
- package/dist/src/annotations/quote.js +40 -12
- package/dist/src/annotations/store.d.ts +0 -1
- package/dist/src/annotations/store.js +66 -14
- package/dist/src/app/article.d.ts +2 -1
- package/dist/src/app/article.js +66 -70
- package/dist/src/app/bundle-files.d.ts +0 -4
- package/dist/src/app/bundle-files.js +11 -14
- package/dist/src/app/markdown.js +18 -6
- package/dist/src/app/source.d.ts +0 -4
- package/dist/src/app/source.js +18 -14
- package/dist/src/cli/arguments.d.ts +2 -8
- package/dist/src/cli/arguments.js +67 -80
- package/dist/src/cli/commands.d.ts +42 -0
- package/dist/src/cli/commands.js +795 -0
- package/dist/src/cli/config.js +12 -9
- package/dist/src/cli/digest.d.ts +1 -1
- package/dist/src/cli/digest.js +39 -17
- package/dist/src/cli/import.js +25 -4
- package/dist/src/cli/indexing.d.ts +2 -0
- package/dist/src/cli/indexing.js +26 -9
- package/dist/src/cli/main.js +47 -386
- package/dist/src/cli/portability.d.ts +0 -2
- package/dist/src/cli/portability.js +13 -8
- package/dist/src/cli/queue.js +12 -8
- package/dist/src/cli/runtimes.d.ts +1 -3
- package/dist/src/cli/runtimes.js +13 -6
- package/dist/src/cli/terminal.js +1 -1
- package/dist/src/cli/worker.d.ts +7 -0
- package/dist/src/cli/worker.js +54 -15
- package/dist/src/derivations/kinds.d.ts +34 -0
- package/dist/src/derivations/kinds.js +14 -0
- package/dist/src/derivations/types.d.ts +48 -0
- package/dist/src/derivations/types.js +1 -0
- package/dist/src/digest/derivation.d.ts +25 -0
- package/dist/src/digest/derivation.js +124 -0
- package/dist/src/digest/editorial.d.ts +10 -46
- package/dist/src/digest/editorial.js +73 -59
- package/dist/src/digest/endpoint.js +63 -27
- package/dist/src/digest/pipeline.d.ts +53 -32
- package/dist/src/digest/pipeline.js +357 -125
- package/dist/src/digest/schema.d.ts +44 -17
- package/dist/src/digest/schema.js +132 -44
- package/dist/src/errors.d.ts +46 -0
- package/dist/src/errors.js +117 -0
- package/dist/src/identity/query-policy.d.ts +0 -5
- package/dist/src/identity/query-policy.js +61 -9
- package/dist/src/identity/uri.d.ts +6 -5
- package/dist/src/identity/uri.js +37 -18
- package/dist/src/identity/visibility.d.ts +11 -1
- package/dist/src/identity/visibility.js +81 -17
- package/dist/src/importers/apply.js +104 -36
- package/dist/src/importers/custody.js +27 -9
- package/dist/src/importers/inspect.js +125 -38
- package/dist/src/importers/jungle-digest.d.ts +31 -24
- package/dist/src/importers/jungle-digest.js +217 -115
- package/dist/src/importers/jungle-store.d.ts +20 -11
- package/dist/src/importers/jungle-store.js +136 -41
- package/dist/src/importers/jungle.d.ts +32 -16
- package/dist/src/importers/jungle.js +145 -32
- package/dist/src/importers/manifest.js +57 -21
- package/dist/src/importers/verify-helper.js +18 -4
- package/dist/src/indexing/image.js +29 -10
- package/dist/src/indexing/index.d.ts +37 -6
- package/dist/src/indexing/index.js +360 -124
- package/dist/src/indexing/jobs.js +156 -47
- package/dist/src/indexing/prune-run.d.ts +2 -0
- package/dist/src/indexing/prune-run.js +69 -26
- package/dist/src/indexing/prune.d.ts +12 -3
- package/dist/src/indexing/prune.js +85 -35
- package/dist/src/indexing/runner.d.ts +2 -1
- package/dist/src/indexing/runner.js +83 -40
- package/dist/src/indexing/upgrade.d.ts +9 -0
- package/dist/src/indexing/upgrade.js +79 -0
- package/dist/src/inference/asr/bridge.js +6 -2
- package/dist/src/inference/asr/protocol.js +109 -19
- package/dist/src/inference/asr/runtime.js +58 -21
- package/dist/src/inference/asr/windows.js +9 -5
- package/dist/src/inference/embedding/index.d.ts +2 -6
- package/dist/src/inference/embedding/index.js +89 -29
- package/dist/src/ingest/attempt.d.ts +29 -0
- package/dist/src/ingest/attempt.js +55 -0
- package/dist/src/media/capture-helper.js +7 -2
- package/dist/src/media/capture.d.ts +2 -1
- package/dist/src/media/capture.js +124 -43
- package/dist/src/media/handler.d.ts +4 -2
- package/dist/src/media/handler.js +88 -70
- package/dist/src/media/transcribe.js +63 -21
- package/dist/src/metadata/article.js +49 -17
- package/dist/src/models/artifacts.d.ts +10 -8
- package/dist/src/models/artifacts.js +237 -106
- package/dist/src/models/catalog.d.ts +2 -5
- package/dist/src/models/catalog.js +94 -25
- package/dist/src/models/download.js +55 -15
- package/dist/src/models/runtime.d.ts +0 -2
- package/dist/src/models/runtime.js +69 -33
- package/dist/src/pdf/capture-helper.js +3 -1
- package/dist/src/pdf/capture.d.ts +2 -1
- package/dist/src/pdf/capture.js +95 -27
- package/dist/src/pdf/errors.js +12 -1
- package/dist/src/pdf/handler.d.ts +2 -1
- package/dist/src/pdf/handler.js +70 -71
- package/dist/src/pdf/index.d.ts +1 -3
- package/dist/src/pdf/index.js +0 -2
- package/dist/src/pdf/input.js +12 -7
- package/dist/src/pdf/normalize.js +80 -21
- package/dist/src/pdf/source-identity.js +3 -2
- package/dist/src/pdf/source.js +12 -8
- package/dist/src/pdf/verify-helper.js +15 -3
- package/dist/src/portability/backup.d.ts +2 -9
- package/dist/src/portability/backup.js +108 -22
- package/dist/src/portability/database.js +51 -17
- package/dist/src/portability/export.js +35 -9
- package/dist/src/portability/files.d.ts +1 -4
- package/dist/src/portability/files.js +127 -67
- package/dist/src/portability/gc.js +65 -22
- package/dist/src/publish/component.js +1 -1
- package/dist/src/publish/feed.d.ts +2 -2
- package/dist/src/publish/feed.js +75 -29
- package/dist/src/publish/identity.d.ts +5 -1
- package/dist/src/publish/identity.js +31 -11
- package/dist/src/publish/index.d.ts +2 -3
- package/dist/src/publish/index.js +34 -21
- package/dist/src/publish/projection.d.ts +2 -9
- package/dist/src/publish/projection.js +21 -14
- package/dist/src/search/extension.js +3 -1
- package/dist/src/search/hybrid.d.ts +3 -8
- package/dist/src/search/hybrid.js +45 -11
- package/dist/src/search/lexical.js +18 -4
- package/dist/src/search/query.js +56 -25
- package/dist/src/search/vector.js +37 -10
- package/dist/src/service/launch-agent.d.ts +5 -3
- package/dist/src/service/launch-agent.js +272 -83
- package/dist/src/service/lifetime.js +47 -14
- package/dist/src/service/loop.d.ts +3 -3
- package/dist/src/service/loop.js +47 -19
- package/dist/src/store/annotations.d.ts +32 -0
- package/dist/src/store/annotations.js +44 -0
- package/dist/src/store/backups.d.ts +18 -0
- package/dist/src/store/backups.js +110 -0
- package/dist/src/store/blobs.d.ts +24 -0
- package/dist/src/store/blobs.js +139 -0
- package/dist/src/store/changes.d.ts +5 -12
- package/dist/src/store/changes.js +108 -56
- package/dist/src/store/context.d.ts +15 -0
- package/dist/src/store/context.js +19 -0
- package/dist/src/store/derivations.d.ts +42 -0
- package/dist/src/store/derivations.js +104 -0
- package/dist/src/store/files.d.ts +0 -2
- package/dist/src/store/files.js +19 -24
- package/dist/src/store/ids.d.ts +30 -0
- package/dist/src/store/ids.js +34 -0
- package/dist/src/store/imports.d.ts +26 -0
- package/dist/src/store/imports.js +57 -0
- package/dist/src/store/index.d.ts +3 -186
- package/dist/src/store/index.js +2 -809
- package/dist/src/store/lease.d.ts +20 -0
- package/dist/src/store/lease.js +62 -0
- package/dist/src/store/library.d.ts +75 -0
- package/dist/src/store/library.js +174 -0
- package/dist/src/store/open.d.ts +7 -0
- package/dist/src/store/open.js +183 -0
- package/dist/src/store/queue.d.ts +65 -0
- package/dist/src/store/queue.js +327 -0
- package/dist/src/store/revisions.d.ts +12 -0
- package/dist/src/store/revisions.js +189 -0
- package/dist/src/store/search-index.d.ts +63 -0
- package/dist/src/store/search-index.js +190 -0
- package/dist/src/store/sync-ready.d.ts +6 -0
- package/dist/src/store/sync-ready.js +121 -0
- package/dist/src/store/types.d.ts +1 -5
- package/dist/src/store/types.js +1 -8
- package/dist/src/util/canonical.d.ts +2 -0
- package/dist/src/util/canonical.js +17 -0
- package/dist/src/util/exact.d.ts +2 -0
- package/dist/src/util/exact.js +9 -0
- package/dist/src/util/fsync.d.ts +4 -0
- package/dist/src/util/fsync.js +22 -0
- package/dist/src/util/hash.d.ts +4 -0
- package/dist/src/util/hash.js +8 -0
- package/dist/src/worker/basic.d.ts +0 -1
- package/dist/src/worker/basic.js +23 -7
- package/dist/src/worker/custody.d.ts +4 -1
- package/dist/src/worker/custody.js +111 -38
- package/dist/src/worker/engine.d.ts +2 -1
- package/dist/src/worker/engine.js +42 -20
- package/dist/src/worker/identity.js +13 -5
- package/dist/src/worker/journal.d.ts +1 -1
- package/dist/src/worker/journal.js +54 -24
- package/dist/src/worker/lock.d.ts +18 -0
- package/dist/src/worker/lock.js +196 -0
- package/dist/src/worker/runtime-gc.d.ts +4 -3
- package/dist/src/worker/runtime-gc.js +68 -41
- package/dist/src/worker/supervisor-child.js +19 -6
- package/dist/src/worker/supervisor.js +41 -8
- package/native/runtime-manifest.json +4 -4
- package/package.json +10 -2
- package/dist/src/digest/store.d.ts +0 -57
- package/dist/src/digest/store.js +0 -77
- package/dist/src/store/foundation.d.ts +0 -7
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
code;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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) {
|
|
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) {
|
|
31
|
-
|
|
32
|
-
|
|
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])) ||
|
|
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) => {
|
|
83
|
-
|
|
84
|
-
|
|
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([
|
|
112
|
-
|
|
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], {
|
|
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 {
|
|
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
|
|
10
|
-
const result = await sdk.acquireEphemeralMedia({
|
|
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
|
-
|
|
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
|
|
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
|
|
13
|
-
|
|
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
|
|
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 () => {
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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' }), {
|
|
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({
|
|
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], {
|
|
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 = {
|
|
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 ||
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
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) ||
|
|
119
|
-
|
|
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 ||
|
|
127
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
221
|
+
throw new SpongeError('integrity');
|
|
158
222
|
}
|
|
159
223
|
if (file.role === 'audio')
|
|
160
|
-
captured.audio = {
|
|
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
|
|
164
|
-
captured.captions = {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
13
|
+
interface MediaResult {
|
|
14
14
|
ok: true;
|
|
15
15
|
itemId: string;
|
|
16
16
|
revisionId: string;
|
|
17
17
|
status: 'complete' | 'partial';
|
|
18
|
-
}
|
|
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 {};
|