@hraness/sponge 0.2.4 → 0.2.6
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 +54 -0
- package/dist/migrations/005.d.ts +3 -0
- package/dist/migrations/005.js +19 -0
- package/dist/migrations/006.d.ts +3 -0
- package/dist/migrations/006.js +9 -0
- package/dist/migrations/007.d.ts +3 -0
- package/dist/migrations/007.js +10 -0
- package/dist/src/annotations/quote.d.ts +32 -0
- package/dist/src/annotations/quote.js +135 -0
- package/dist/src/annotations/store.d.ts +50 -0
- package/dist/src/annotations/store.js +89 -0
- package/dist/src/app/source.d.ts +2 -0
- package/dist/src/app/source.js +13 -17
- package/dist/src/cli/arguments.js +24 -7
- package/dist/src/cli/digest.d.ts +10 -0
- package/dist/src/cli/digest.js +63 -0
- package/dist/src/cli/import.d.ts +11 -0
- package/dist/src/cli/import.js +10 -0
- package/dist/src/cli/indexing.d.ts +3 -0
- package/dist/src/cli/indexing.js +5 -2
- package/dist/src/cli/main.js +94 -8
- package/dist/src/cli/runtimes.d.ts +10 -0
- package/dist/src/cli/runtimes.js +32 -0
- package/dist/src/digest/editorial.d.ts +54 -0
- package/dist/src/digest/editorial.js +87 -0
- package/dist/src/digest/endpoint.d.ts +20 -0
- package/dist/src/digest/endpoint.js +103 -0
- package/dist/src/digest/pipeline.d.ts +148 -0
- package/dist/src/digest/pipeline.js +399 -0
- package/dist/src/digest/schema.d.ts +126 -0
- package/dist/src/digest/schema.js +138 -0
- package/dist/src/digest/store.d.ts +57 -0
- package/dist/src/digest/store.js +79 -0
- package/dist/src/identity/query-policy.d.ts +21 -0
- package/dist/src/identity/query-policy.js +55 -0
- package/dist/src/identity/uri.d.ts +30 -0
- package/dist/src/identity/uri.js +94 -0
- package/dist/src/identity/visibility.d.ts +24 -0
- package/dist/src/identity/visibility.js +75 -0
- package/dist/src/importers/apply.d.ts +10 -0
- package/dist/src/importers/apply.js +25 -22
- package/dist/src/importers/jungle-digest.d.ts +107 -0
- package/dist/src/importers/jungle-digest.js +196 -0
- package/dist/src/importers/jungle-store.d.ts +45 -0
- package/dist/src/importers/jungle-store.js +96 -0
- package/dist/src/importers/jungle.d.ts +70 -0
- package/dist/src/importers/jungle.js +142 -0
- package/dist/src/indexing/index.d.ts +7 -0
- package/dist/src/indexing/index.js +31 -8
- package/dist/src/indexing/prune-run.d.ts +22 -0
- package/dist/src/indexing/prune-run.js +67 -0
- package/dist/src/indexing/prune.d.ts +30 -0
- package/dist/src/indexing/prune.js +95 -0
- package/dist/src/indexing/runner.js +5 -5
- package/dist/src/inference/embedding/index.d.ts +25 -0
- package/dist/src/inference/embedding/index.js +11 -0
- package/dist/src/models/artifacts.js +175 -33
- package/dist/src/portability/backup.js +3 -1
- package/dist/src/portability/database.js +3 -0
- package/dist/src/publish/component.d.ts +8 -0
- package/dist/src/publish/component.js +114 -0
- package/dist/src/publish/feed.d.ts +24 -0
- package/dist/src/publish/feed.js +77 -0
- package/dist/src/publish/identity.d.ts +6 -0
- package/dist/src/publish/identity.js +68 -0
- package/dist/src/publish/index.d.ts +34 -0
- package/dist/src/publish/index.js +137 -0
- package/dist/src/publish/projection.d.ts +29 -0
- package/dist/src/publish/projection.js +43 -0
- package/dist/src/search/hybrid.js +2 -2
- package/dist/src/search/lexical.d.ts +11 -0
- package/dist/src/search/lexical.js +6 -0
- package/dist/src/search/query.d.ts +7 -3
- package/dist/src/search/query.js +52 -33
- package/dist/src/search/vector.d.ts +1 -1
- package/dist/src/search/vector.js +6 -5
- package/dist/src/store/changes.d.ts +42 -0
- package/dist/src/store/changes.js +178 -0
- package/dist/src/store/foundation.d.ts +7 -0
- package/dist/src/store/foundation.js +7 -0
- package/dist/src/store/index.d.ts +69 -0
- package/dist/src/store/index.js +134 -51
- package/dist/src/worker/runtime-gc.d.ts +30 -0
- package/dist/src/worker/runtime-gc.js +296 -0
- package/native/runtime-manifest.json +4 -4
- package/package.json +1 -1
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { lstat, readdir, realpath, rename, rm, link, unlink, open } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
+
import { StoreError } from '../store/index.js';
|
|
6
|
+
import { modelSetupPlan } from '../models/artifacts.js';
|
|
7
|
+
import { acquireWorkerCustody } from './custody.js';
|
|
8
|
+
import { readJournal, writeJournal, syncDir } from './journal.js';
|
|
9
|
+
import { processIdentity, identityAlive } from './identity.js';
|
|
10
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
11
|
+
const patterns = { capture: /^capture-[a-z0-9][a-z0-9.-]{0,99}$/, model: /^[a-f0-9]{64}$/, worker: uuid };
|
|
12
|
+
const helperNames = ['sponge-speech', 'sponge-embedding'];
|
|
13
|
+
const trash = /^\.gc-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
14
|
+
// A worker writes owner.json just after creating its directory; a young unreadable directory may be mid-acquisition.
|
|
15
|
+
const youngMs = 10 * 60 * 1000;
|
|
16
|
+
const integrity = () => new StoreError('integrity');
|
|
17
|
+
const missing = (error) => error.code === 'ENOENT';
|
|
18
|
+
/** Disk blocks removing one tree frees, never following links: each inode once, and a file only when all of its
|
|
19
|
+
* hard links are inside the tree (a model artifact shared with a kept package frees nothing). */
|
|
20
|
+
async function diskBytes(root) {
|
|
21
|
+
let total = 0;
|
|
22
|
+
const files = new Map();
|
|
23
|
+
const visit = async (path) => {
|
|
24
|
+
let stat;
|
|
25
|
+
try {
|
|
26
|
+
stat = await lstat(path);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (missing(error))
|
|
30
|
+
return;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
if (stat.isDirectory()) {
|
|
34
|
+
total += stat.blocks * 512;
|
|
35
|
+
for (const name of await readdir(path))
|
|
36
|
+
await visit(join(path, name));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const key = `${stat.dev}:${stat.ino}`, file = files.get(key);
|
|
40
|
+
if (file)
|
|
41
|
+
file.seen++;
|
|
42
|
+
else
|
|
43
|
+
files.set(key, { blocks: stat.blocks, nlink: stat.nlink, seen: 1 });
|
|
44
|
+
};
|
|
45
|
+
await visit(root);
|
|
46
|
+
for (const file of files.values())
|
|
47
|
+
if (file.seen >= file.nlink)
|
|
48
|
+
total += file.blocks * 512;
|
|
49
|
+
return total;
|
|
50
|
+
}
|
|
51
|
+
/** The expected parent: a real, private, owned directory at its canonical path, or absent. */
|
|
52
|
+
async function parent(path) {
|
|
53
|
+
let stat;
|
|
54
|
+
try {
|
|
55
|
+
stat = await lstat(path);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (missing(error))
|
|
59
|
+
return null;
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid?.() || (stat.mode & 0o077) !== 0 || await realpath(path) !== path)
|
|
63
|
+
throw integrity();
|
|
64
|
+
return path;
|
|
65
|
+
}
|
|
66
|
+
async function exists(path) { try {
|
|
67
|
+
await lstat(path);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (missing(error))
|
|
72
|
+
return false;
|
|
73
|
+
throw error;
|
|
74
|
+
} }
|
|
75
|
+
async function fileSha256(path, maxBytes) {
|
|
76
|
+
let file;
|
|
77
|
+
try {
|
|
78
|
+
file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const stat = await file.stat();
|
|
85
|
+
if (!stat.isFile() || stat.size > maxBytes)
|
|
86
|
+
return null;
|
|
87
|
+
return createHash('sha256').update(await file.readFile()).digest('hex');
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
await file.close();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Direct children whose names match the kind's pattern; symlinks and foreign entries are reported, never followed. */
|
|
94
|
+
async function children(kind, dir, kept) {
|
|
95
|
+
if (!dir)
|
|
96
|
+
return [];
|
|
97
|
+
const result = [];
|
|
98
|
+
for (const name of (await readdir(dir)).sort()) {
|
|
99
|
+
if (!patterns[kind].test(name) && !trash.test(name))
|
|
100
|
+
continue;
|
|
101
|
+
const stat = await lstat(join(dir, name));
|
|
102
|
+
if (stat.isSymbolicLink()) {
|
|
103
|
+
kept.push({ kind, name, reason: 'symlink' });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (!stat.isDirectory() || stat.uid !== process.getuid?.()) {
|
|
107
|
+
kept.push({ kind, name, reason: 'foreign' });
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
result.push({ kind, dir, name });
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
/** Rename first so a swapped-in link is caught before recursive removal; rm never follows links inside. */
|
|
115
|
+
async function remove(candidate) {
|
|
116
|
+
const path = join(candidate.dir, candidate.name), moved = join(candidate.dir, `.gc-${randomUUID()}`);
|
|
117
|
+
const before = await lstat(path);
|
|
118
|
+
if (before.isSymbolicLink() || !before.isDirectory())
|
|
119
|
+
return false;
|
|
120
|
+
await rename(path, moved);
|
|
121
|
+
const after = await lstat(moved);
|
|
122
|
+
if (after.isSymbolicLink() || !after.isDirectory() || after.ino !== before.ino || after.dev !== before.dev) {
|
|
123
|
+
await rename(moved, path);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
await syncDir(candidate.dir);
|
|
127
|
+
await rm(moved, { recursive: true });
|
|
128
|
+
await syncDir(candidate.dir);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
async function workerInUse(dir, name) {
|
|
132
|
+
try {
|
|
133
|
+
const owner = await readJournal(join(dir, name, 'owner.json'));
|
|
134
|
+
if (owner?.schemaVersion !== 1 || owner.incarnation !== name || !owner.identity)
|
|
135
|
+
return true;
|
|
136
|
+
return await identityAlive(owner.identity);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (!missing(error))
|
|
140
|
+
return true;
|
|
141
|
+
try {
|
|
142
|
+
return Date.now() - (await lstat(join(dir, name))).mtimeMs < youngMs;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** Remove only superseded capture runtimes, model packages and settled worker journals.
|
|
150
|
+
* Applying holds worker custody, so no worker, capture, import verification, index run or query
|
|
151
|
+
* can start or be running; each model directory is additionally taken under its setup lock. */
|
|
152
|
+
export async function collectRuntimes(store, options) {
|
|
153
|
+
const dryRun = options.dryRun === true, base = await realpath(store.root), runtime = await parent(join(base, 'runtime'));
|
|
154
|
+
if (store.ownerSnapshot())
|
|
155
|
+
throw new StoreError('owner-busy');
|
|
156
|
+
if (store.pendingCleanupCount())
|
|
157
|
+
throw Object.assign(Error('cleanup-required'), { code: 'cleanup-required' });
|
|
158
|
+
const lockPath = join(base, 'runtime', 'worker.lock');
|
|
159
|
+
// Checked first so a refused collection leaves no custody journal; acquisition stays the authority.
|
|
160
|
+
if (await exists(lockPath))
|
|
161
|
+
throw new StoreError('owner-busy');
|
|
162
|
+
const custody = dryRun || !runtime ? undefined : await acquireWorkerCustody(base);
|
|
163
|
+
const removed = [], kept = [];
|
|
164
|
+
try {
|
|
165
|
+
if (!runtime)
|
|
166
|
+
return { dryRun, removed, kept, bytes: 0 };
|
|
167
|
+
// Recheck under custody: an owner that appeared before our lock still blocks.
|
|
168
|
+
if (store.ownerSnapshot())
|
|
169
|
+
throw new StoreError('owner-busy');
|
|
170
|
+
if (store.pendingCleanupCount())
|
|
171
|
+
throw Object.assign(Error('cleanup-required'), { code: 'cleanup-required' });
|
|
172
|
+
const dirs = { capture: await parent(join(runtime, 'capture')), model: await parent(join(runtime, 'models')), worker: await parent(join(runtime, 'workers')) };
|
|
173
|
+
const references = store.runtimeReferences(), models = options.models;
|
|
174
|
+
const modelKeep = new Set(models?.fingerprints ?? []);
|
|
175
|
+
for (const artifact of references.modelArtifacts) {
|
|
176
|
+
if (patterns.model.test(artifact.id))
|
|
177
|
+
modelKeep.add(artifact.id);
|
|
178
|
+
try {
|
|
179
|
+
modelKeep.add(modelSetupPlan(JSON.parse(artifact.manifest)).fingerprint);
|
|
180
|
+
}
|
|
181
|
+
catch { /* An unparsable row still pins its id. */ }
|
|
182
|
+
}
|
|
183
|
+
// A generation's helper is satisfied only by an installed kept package, never by an uninstalled catalog entry.
|
|
184
|
+
const helpers = new Set(references.embeddingHelpers), installed = new Set();
|
|
185
|
+
for (const fp of modelKeep) {
|
|
186
|
+
if (!dirs.model || !await exists(join(dirs.model, fp)))
|
|
187
|
+
continue;
|
|
188
|
+
for (const helper of helperNames) {
|
|
189
|
+
if (await exists(join(dirs.model, fp, helper)))
|
|
190
|
+
installed.add(helper);
|
|
191
|
+
}
|
|
192
|
+
helpers.delete(await fileSha256(join(dirs.model, fp, 'sponge-embedding'), 32 * 1024 ** 2) ?? '');
|
|
193
|
+
}
|
|
194
|
+
const take = async (candidate) => {
|
|
195
|
+
const bytes = await diskBytes(join(candidate.dir, candidate.name));
|
|
196
|
+
if (dryRun || await remove(candidate))
|
|
197
|
+
removed.push({ kind: candidate.kind, name: candidate.name, bytes });
|
|
198
|
+
else
|
|
199
|
+
kept.push({ kind: candidate.kind, name: candidate.name, reason: 'symlink' });
|
|
200
|
+
};
|
|
201
|
+
for (const candidate of await children('capture', dirs.capture, kept)) {
|
|
202
|
+
if (options.captureIds.includes(candidate.name))
|
|
203
|
+
kept.push({ kind: 'capture', name: candidate.name, reason: 'referenced' });
|
|
204
|
+
else
|
|
205
|
+
await take(candidate);
|
|
206
|
+
}
|
|
207
|
+
for (const candidate of await children('model', dirs.model, kept)) {
|
|
208
|
+
const { name, dir } = candidate;
|
|
209
|
+
if (trash.test(name)) {
|
|
210
|
+
await take(candidate);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (!models) {
|
|
214
|
+
kept.push({ kind: 'model', name, reason: 'catalog-unavailable' });
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (modelKeep.has(name)) {
|
|
218
|
+
kept.push({ kind: 'model', name, reason: 'referenced' });
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const helper = join(dir, name, 'sponge-embedding');
|
|
222
|
+
if ((references.unmappedSpaces && await exists(helper)) || helpers.size && helpers.has(await fileSha256(helper, 32 * 1024 ** 2) ?? '')) {
|
|
223
|
+
kept.push({ kind: 'model', name, reason: 'referenced' });
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
// Until this Sponge's package of a kind is installed, older packages of that kind are its only local copy.
|
|
227
|
+
let missingKind = false;
|
|
228
|
+
for (const helper of helperNames)
|
|
229
|
+
if (!installed.has(helper) && await exists(join(dir, name, helper)))
|
|
230
|
+
missingKind = true;
|
|
231
|
+
if (missingKind) {
|
|
232
|
+
kept.push({ kind: 'model', name, reason: 'current-missing' });
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const lock = join(dir, `.${name}.lock`);
|
|
236
|
+
if (await exists(lock) || await exists(join(dir, `.${name}.recovery-lock`))) {
|
|
237
|
+
kept.push({ kind: 'model', name, reason: 'in-use' });
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (dryRun) {
|
|
241
|
+
await take(candidate);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
// The model setup lock: a concurrent setup or recovery of this fingerprint sees owner-busy.
|
|
245
|
+
const identity = await processIdentity(process.pid);
|
|
246
|
+
if (!identity)
|
|
247
|
+
throw integrity();
|
|
248
|
+
const incarnation = randomUUID(), ownerFile = join(dir, `.model-owner-${incarnation}.json`);
|
|
249
|
+
await writeJournal(ownerFile, { schemaVersion: 1, incarnation, fingerprint: name, stage: '.model-install-' + incarnation, identity });
|
|
250
|
+
try {
|
|
251
|
+
try {
|
|
252
|
+
await link(ownerFile, lock);
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
if (error.code === 'EEXIST') {
|
|
256
|
+
kept.push({ kind: 'model', name, reason: 'in-use' });
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
await syncDir(dir);
|
|
263
|
+
await take(candidate);
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
await unlink(lock);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
await unlink(ownerFile);
|
|
271
|
+
await syncDir(dir);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
for (const candidate of await children('worker', dirs.worker, kept)) {
|
|
275
|
+
const { name, dir } = candidate;
|
|
276
|
+
if (name === custody?.incarnation)
|
|
277
|
+
continue;
|
|
278
|
+
if (!trash.test(name) && await workerInUse(dir, name)) {
|
|
279
|
+
kept.push({ kind: 'worker', name, reason: 'in-use' });
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
await take(candidate);
|
|
283
|
+
}
|
|
284
|
+
return { dryRun, removed, kept, bytes: removed.reduce((n, e) => n + e.bytes, 0) };
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
if (custody) {
|
|
288
|
+
await custody.release();
|
|
289
|
+
try {
|
|
290
|
+
await rm(custody.directory, { recursive: true });
|
|
291
|
+
await syncDir(join(base, 'runtime', 'workers'));
|
|
292
|
+
}
|
|
293
|
+
catch { /* The next collection removes this settled journal. */ }
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -27,14 +27,14 @@
|
|
|
27
27
|
}
|
|
28
28
|
],
|
|
29
29
|
"build": {
|
|
30
|
-
"asrSourceSha256": "
|
|
30
|
+
"asrSourceSha256": "e944e4e0b1c2cf571e78a83fcfbf56a91a14fd08dcc701562d32555cf090ec40",
|
|
31
31
|
"clangVersion": "Apple clang version 21.0.0 (clang-2100.1.1.101)",
|
|
32
|
-
"embeddingSourceSha256": "
|
|
32
|
+
"embeddingSourceSha256": "7d58718911725b3c3dce35a09a253704b4e5e732cf1e23c12389485127534b10",
|
|
33
33
|
"swiftVersion": "Apple Swift version 6.3.3 (swiftlang-6.3.3.1.3 clang-2100.1.1.101)"
|
|
34
34
|
},
|
|
35
35
|
"minimumMacOS": "26.0",
|
|
36
|
-
"packageVersion": "0.2.
|
|
36
|
+
"packageVersion": "0.2.6",
|
|
37
37
|
"platform": "darwin",
|
|
38
38
|
"schemaVersion": 1,
|
|
39
|
-
"sourceCommit": "
|
|
39
|
+
"sourceCommit": "1190f23ad793985b6934583c5d8b0957d79a4ce0"
|
|
40
40
|
}
|