@equationalapplications/core-llm-wiki 4.6.1 → 4.8.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.
@@ -0,0 +1,2547 @@
1
+ import MiniSearch from 'minisearch';
2
+
3
+ var __typeError = (msg) => {
4
+ throw TypeError(msg);
5
+ };
6
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
7
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
8
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
9
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
10
+
11
+ // src/utils/embedding.ts
12
+ function parseEmbedding(blob, text) {
13
+ if (blob && blob.byteLength > 0) {
14
+ if (blob.byteLength % 4 !== 0) return null;
15
+ const copy = new ArrayBuffer(blob.byteLength);
16
+ new Uint8Array(copy).set(blob);
17
+ const vector = new Float32Array(copy);
18
+ for (const value of vector) {
19
+ if (!Number.isFinite(value)) return null;
20
+ }
21
+ return vector;
22
+ }
23
+ if (text) {
24
+ try {
25
+ const arr = JSON.parse(text);
26
+ if (!Array.isArray(arr) || !arr.every((v) => typeof v === "number" && isFinite(v))) return null;
27
+ const vector = new Float32Array(arr);
28
+ for (const value of vector) {
29
+ if (!Number.isFinite(value)) return null;
30
+ }
31
+ return vector;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+
39
+ // src/utils/cosine.ts
40
+ function cosineSimilarity(a, b) {
41
+ let dot = 0, normA = 0, normB = 0;
42
+ const len = Math.min(a.length, b.length);
43
+ for (let i = 0; i < len; i++) {
44
+ dot += a[i] * b[i];
45
+ normA += a[i] * a[i];
46
+ normB += b[i] * b[i];
47
+ }
48
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
49
+ return denom === 0 ? 0 : dot / denom;
50
+ }
51
+
52
+ // src/services/SearchService.ts
53
+ var _SearchService = class _SearchService {
54
+ constructor(entryRepo) {
55
+ this.entryRepo = entryRepo;
56
+ this.miniSearchEntryIdsByEntity = /* @__PURE__ */ new Map();
57
+ this.vectorCache = /* @__PURE__ */ new Map();
58
+ this.miniSearch = new MiniSearch({
59
+ fields: ["title", "body", "tags"],
60
+ storeFields: ["entity_id"],
61
+ searchOptions: {
62
+ boost: { title: 2 },
63
+ fuzzy: 0.2,
64
+ prefix: true
65
+ }
66
+ });
67
+ }
68
+ /**
69
+ * Rebuilds the search index and clears the vector cache for a given entity.
70
+ * A direct replacement for manually syncing state after a DB transaction.
71
+ */
72
+ async sync(entityId) {
73
+ await this.rebuildIndex(entityId);
74
+ this.evictCache(entityId);
75
+ }
76
+ /**
77
+ * Clears the parsed vector cache. Useful for mid-loop flush guarantees
78
+ * or memory pressure evictions.
79
+ */
80
+ evictCache(entityId) {
81
+ if (entityId) {
82
+ this.vectorCache.delete(entityId);
83
+ } else {
84
+ this.vectorCache.clear();
85
+ }
86
+ }
87
+ /**
88
+ * Fully resets the search service.
89
+ */
90
+ clearAll() {
91
+ this.vectorCache.clear();
92
+ this.miniSearch.removeAll();
93
+ this.miniSearchEntryIdsByEntity.clear();
94
+ }
95
+ /**
96
+ * Executes a keyword search against the active MiniSearch index.
97
+ */
98
+ searchKeyword(query, entityIds, limit) {
99
+ const entityIdSet = new Set(entityIds);
100
+ const results = this.miniSearch.search(query, {
101
+ filter: (r) => entityIdSet.has(r.entity_id),
102
+ combineWith: "OR"
103
+ });
104
+ return results.slice(0, limit);
105
+ }
106
+ /**
107
+ * Pre-fetches MiniSearch scores for candidate hydration, used during hybrid weighting.
108
+ */
109
+ getMiniSearchScores(query, entityIds, preFilterLimit) {
110
+ const entityIdSet = new Set(entityIds);
111
+ let results = this.miniSearch.search(query, {
112
+ filter: (r) => entityIdSet.has(r.entity_id),
113
+ combineWith: "OR"
114
+ });
115
+ if (preFilterLimit !== void 0) {
116
+ results = results.slice(0, preFilterLimit);
117
+ }
118
+ if (results.length === 0) return /* @__PURE__ */ new Map();
119
+ const maxMsScore = Math.max(1, results[0]?.score ?? 1);
120
+ return new Map(results.map((r) => [r.id, r.score / maxMsScore]));
121
+ }
122
+ /**
123
+ * Score candidate rows using in-process JS cosine similarity.
124
+ * Applies hybrid blending (if weight set) and tie-break sorting before returning.
125
+ */
126
+ async rankSemantic(args) {
127
+ const queryVec = args.queryVec instanceof Float32Array ? args.queryVec.slice() : Array.from(args.queryVec);
128
+ const { entityId, candidateRows, weight, miniSearchScores, populateCache, limit, skipSort } = args;
129
+ let entityCache = this.vectorCache.get(entityId);
130
+ const tooLarge = populateCache && candidateRows.length > _SearchService.MAX_VECTOR_CACHE_FACTS_PER_ENTITY;
131
+ if (tooLarge && entityCache) {
132
+ this.vectorCache.delete(entityId);
133
+ entityCache = void 0;
134
+ }
135
+ const canCache = populateCache && !tooLarge;
136
+ if (canCache && !entityCache) {
137
+ entityCache = /* @__PURE__ */ new Map();
138
+ }
139
+ const scored = candidateRows.map((row) => {
140
+ let vector = entityCache?.get(row.id) ?? parseEmbedding(row.embedding_blob, row.embedding);
141
+ if (vector && canCache && entityCache && !entityCache.has(row.id)) {
142
+ entityCache.set(row.id, vector);
143
+ }
144
+ let score = 0;
145
+ if (vector && vector.length === queryVec.length) {
146
+ const cosSim = cosineSimilarity(queryVec, vector);
147
+ if (weight !== void 0) {
148
+ const kwScore = miniSearchScores?.get(row.id) ?? 0;
149
+ score = weight * Math.max(0, cosSim) + (1 - weight) * kwScore;
150
+ } else {
151
+ score = cosSim;
152
+ }
153
+ } else if (weight !== void 0 && weight < 1) {
154
+ const kwScore = miniSearchScores?.get(row.id) ?? 0;
155
+ score = (1 - weight) * kwScore;
156
+ } else {
157
+ score = -2;
158
+ }
159
+ return {
160
+ id: row.id,
161
+ entity_id: row.entity_id,
162
+ score,
163
+ updated_at: row.updated_at,
164
+ access_count: row.access_count
165
+ };
166
+ });
167
+ if (canCache && entityCache && entityCache.size > 0) {
168
+ if (!this.vectorCache.has(entityId)) {
169
+ if (this.vectorCache.size >= _SearchService.MAX_VECTOR_CACHE_ENTITIES) {
170
+ const oldestKey = this.vectorCache.keys().next().value;
171
+ if (oldestKey !== void 0) this.vectorCache.delete(oldestKey);
172
+ }
173
+ this.vectorCache.set(entityId, entityCache);
174
+ }
175
+ }
176
+ if (!skipSort) {
177
+ this._tieBreakSort(scored);
178
+ }
179
+ return scored.slice(0, limit);
180
+ }
181
+ // --- Internal Index Management ---
182
+ async rebuildIndex(entityId) {
183
+ if (entityId) {
184
+ const rows2 = await this.entryRepo.findMiniSearchRows(entityId);
185
+ const previousIds = this.miniSearchEntryIdsByEntity.get(entityId);
186
+ if (previousIds) {
187
+ for (const id of previousIds) {
188
+ this.miniSearch.discard(id);
189
+ }
190
+ }
191
+ const documents2 = rows2.map((row) => this.normalizeMiniSearchRow(row));
192
+ if (documents2.length > 0) {
193
+ this.miniSearch.addAll(documents2);
194
+ }
195
+ this.miniSearchEntryIdsByEntity.set(
196
+ entityId,
197
+ new Set(documents2.map((document) => document.id))
198
+ );
199
+ return;
200
+ }
201
+ const rows = await this.entryRepo.findMiniSearchRows();
202
+ this.miniSearch.removeAll();
203
+ this.miniSearchEntryIdsByEntity.clear();
204
+ const documents = rows.map((row) => this.normalizeMiniSearchRow(row));
205
+ if (documents.length > 0) {
206
+ this.miniSearch.addAll(documents);
207
+ }
208
+ for (const document of documents) {
209
+ const ids = this.miniSearchEntryIdsByEntity.get(document.entity_id) ?? /* @__PURE__ */ new Set();
210
+ ids.add(document.id);
211
+ this.miniSearchEntryIdsByEntity.set(document.entity_id, ids);
212
+ }
213
+ }
214
+ normalizeMiniSearchRow(row) {
215
+ return {
216
+ id: row.id,
217
+ entity_id: row.entity_id,
218
+ title: row.title,
219
+ body: row.body,
220
+ tags: (() => {
221
+ try {
222
+ const parsed = JSON.parse(row.tags);
223
+ return Array.isArray(parsed) ? parsed.join(" ") : row.tags;
224
+ } catch {
225
+ return row.tags;
226
+ }
227
+ })()
228
+ };
229
+ }
230
+ _tieBreakSort(items) {
231
+ items.sort((a, b) => this._compareScoredRows(a, b));
232
+ }
233
+ _compareScoredRows(a, b) {
234
+ const scoreDiff = b.score - a.score;
235
+ if (!Number.isNaN(scoreDiff) && scoreDiff !== 0) return scoreDiff;
236
+ const accessCountDiff = (b.access_count ?? 0) - (a.access_count ?? 0);
237
+ if (accessCountDiff !== 0) return accessCountDiff;
238
+ const updatedAtDiff = (b.updated_at ?? 0) - (a.updated_at ?? 0);
239
+ if (updatedAtDiff !== 0) return updatedAtDiff;
240
+ return a.id.localeCompare(b.id);
241
+ }
242
+ };
243
+ /**
244
+ * Maximum number of entities whose parsed embedding vectors are held in
245
+ * memory. This cap is intentionally conservative so the cache remains safe
246
+ * on memory-constrained runtimes (e.g., mobile/Expo).
247
+ */
248
+ _SearchService.MAX_VECTOR_CACHE_ENTITIES = 16;
249
+ /**
250
+ * Maximum number of fact vectors cached per entity. Keep this high enough to
251
+ * preserve the parsed-embedding reuse optimization for common mid-sized
252
+ * entities while still maintaining a bounded memory footprint.
253
+ */
254
+ _SearchService.MAX_VECTOR_CACHE_FACTS_PER_ENTITY = 500;
255
+ var SearchService = _SearchService;
256
+
257
+ // src/types.ts
258
+ var WikiBusyError = class extends Error {
259
+ constructor(operation, entityId) {
260
+ super(`${operation} already running for entity ${entityId}`);
261
+ this.name = "WikiBusyError";
262
+ this.operation = operation;
263
+ this.entityId = entityId;
264
+ }
265
+ };
266
+ var PrunePartialFailureError = class extends Error {
267
+ constructor(deleted, failedAt, remaining, cause, deletedTasks = 0, deletedEvents = 0) {
268
+ super(`Prune partially failed: deleted ${deleted}, failed at ${failedAt}, ${remaining} remaining`);
269
+ this.name = "PrunePartialFailureError";
270
+ this.deleted = deleted;
271
+ this.failedAt = failedAt;
272
+ this.remaining = remaining;
273
+ this.deletedTasks = deletedTasks;
274
+ this.deletedEvents = deletedEvents;
275
+ this.cause = cause;
276
+ }
277
+ };
278
+ var HOOK_TIMEOUT_MARKER = /* @__PURE__ */ Symbol("WikiMemoryHookTimeout");
279
+
280
+ // src/services/JobManager.ts
281
+ var JobManager = class {
282
+ constructor(prefix) {
283
+ this.prefix = prefix;
284
+ this.activeMaintenanceJobs = /* @__PURE__ */ new Set();
285
+ this.activeIngestJobs = /* @__PURE__ */ new Map();
286
+ this.statusSubscribers = /* @__PURE__ */ new Map();
287
+ }
288
+ _pruneKey(entityId) {
289
+ return `${this.prefix}:${entityId}:prune`;
290
+ }
291
+ _reembedKey(entityId) {
292
+ return `${this.prefix}:${entityId}:reembed`;
293
+ }
294
+ _globalReembedKey() {
295
+ return `${this.prefix}:reembed`;
296
+ }
297
+ _importKey(entityId) {
298
+ return `${this.prefix}:${entityId}:import`;
299
+ }
300
+ _globalImportKey() {
301
+ return `${this.prefix}:import`;
302
+ }
303
+ _forgetKey(entityId) {
304
+ return `${this.prefix}:${entityId}:forget`;
305
+ }
306
+ _librarianKey(entityId) {
307
+ return `${this.prefix}:${entityId}:librarian`;
308
+ }
309
+ _healKey(entityId) {
310
+ return `${this.prefix}:${entityId}:heal`;
311
+ }
312
+ _isReembedActive(entityId) {
313
+ return this.activeMaintenanceJobs.has(this._reembedKey(entityId)) || this.activeMaintenanceJobs.has(this._globalReembedKey());
314
+ }
315
+ _isImportActiveFor(entityId) {
316
+ return this.activeMaintenanceJobs.has(this._importKey(entityId)) || this.activeMaintenanceJobs.has(this._globalImportKey());
317
+ }
318
+ _isForgetActiveFor(entityId) {
319
+ return this.activeMaintenanceJobs.has(this._forgetKey(entityId));
320
+ }
321
+ _isAnyMaintenanceActiveWithSuffix(suffix) {
322
+ const entityKeyPrefix = `${this.prefix}:`;
323
+ for (const k of this.activeMaintenanceJobs) {
324
+ if (k.startsWith(entityKeyPrefix) && k.endsWith(suffix)) return true;
325
+ }
326
+ return false;
327
+ }
328
+ _hasIngestJob(entityId, sourceRef) {
329
+ return this.activeIngestJobs.get(entityId)?.has(sourceRef ?? "") ?? false;
330
+ }
331
+ _addIngestJob(entityId, sourceRef) {
332
+ const sourceKey = sourceRef ?? "";
333
+ let refs = this.activeIngestJobs.get(entityId);
334
+ if (!refs) {
335
+ refs = /* @__PURE__ */ new Set();
336
+ this.activeIngestJobs.set(entityId, refs);
337
+ }
338
+ refs.add(sourceKey);
339
+ }
340
+ _removeIngestJob(entityId, sourceRef) {
341
+ const sourceKey = sourceRef ?? "";
342
+ const refs = this.activeIngestJobs.get(entityId);
343
+ if (!refs) return;
344
+ refs.delete(sourceKey);
345
+ if (refs.size === 0) {
346
+ this.activeIngestJobs.delete(entityId);
347
+ }
348
+ }
349
+ _isIngestActiveFor(entityId) {
350
+ return this.activeIngestJobs.has(entityId);
351
+ }
352
+ acquireLock(operation, entityId, sourceRef) {
353
+ let blockingOperation = null;
354
+ if (operation !== "global_import" && this.activeMaintenanceJobs.has(this._globalImportKey())) {
355
+ throw new WikiBusyError("import", "*");
356
+ }
357
+ switch (operation) {
358
+ case "prune":
359
+ if (this.activeMaintenanceJobs.has(this._pruneKey(entityId))) blockingOperation = "prune";
360
+ else if (this.activeMaintenanceJobs.has(this._librarianKey(entityId))) blockingOperation = "librarian";
361
+ else if (this.activeMaintenanceJobs.has(this._healKey(entityId))) blockingOperation = "heal";
362
+ else if (this._isReembedActive(entityId)) blockingOperation = "reembed";
363
+ else if (this._isIngestActiveFor(entityId)) blockingOperation = "ingest";
364
+ else if (this._isImportActiveFor(entityId)) blockingOperation = "import";
365
+ else if (this._isForgetActiveFor(entityId)) blockingOperation = "forget";
366
+ break;
367
+ case "librarian":
368
+ case "heal": {
369
+ const opKey = operation === "librarian" ? this._librarianKey(entityId) : this._healKey(entityId);
370
+ if (this.activeMaintenanceJobs.has(opKey)) blockingOperation = operation;
371
+ else if (this.activeMaintenanceJobs.has(this._pruneKey(entityId))) blockingOperation = "prune";
372
+ else if (this._isReembedActive(entityId)) blockingOperation = "reembed";
373
+ else if (this._isImportActiveFor(entityId)) blockingOperation = "import";
374
+ else if (this._isForgetActiveFor(entityId)) blockingOperation = "forget";
375
+ break;
376
+ }
377
+ case "reembed":
378
+ if (this.activeMaintenanceJobs.has(this._reembedKey(entityId))) blockingOperation = "reembed";
379
+ else if (this.activeMaintenanceJobs.has(this._globalReembedKey())) blockingOperation = "reembed";
380
+ else if (this.activeMaintenanceJobs.has(this._pruneKey(entityId))) blockingOperation = "prune";
381
+ else if (this.activeMaintenanceJobs.has(this._librarianKey(entityId))) blockingOperation = "librarian";
382
+ else if (this.activeMaintenanceJobs.has(this._healKey(entityId))) blockingOperation = "heal";
383
+ else if (this._isIngestActiveFor(entityId)) blockingOperation = "ingest";
384
+ else if (this._isImportActiveFor(entityId)) blockingOperation = "import";
385
+ else if (this._isForgetActiveFor(entityId)) blockingOperation = "forget";
386
+ break;
387
+ case "global_reembed":
388
+ if (this.activeMaintenanceJobs.has(this._globalReembedKey())) blockingOperation = "reembed";
389
+ else if (this._isAnyMaintenanceActiveWithSuffix(":reembed")) blockingOperation = "reembed";
390
+ else if (this._isAnyMaintenanceActiveWithSuffix(":prune")) blockingOperation = "prune";
391
+ else if (this._isAnyMaintenanceActiveWithSuffix(":librarian")) blockingOperation = "librarian";
392
+ else if (this._isAnyMaintenanceActiveWithSuffix(":heal")) blockingOperation = "heal";
393
+ else if (this.activeIngestJobs.size > 0) blockingOperation = "ingest";
394
+ else if (this._isAnyMaintenanceActiveWithSuffix(":import")) blockingOperation = "import";
395
+ else if (this._isAnyMaintenanceActiveWithSuffix(":forget")) blockingOperation = "forget";
396
+ break;
397
+ case "import":
398
+ case "forget": {
399
+ const selfKey = operation === "import" ? this._importKey(entityId) : this._forgetKey(entityId);
400
+ if (this.activeMaintenanceJobs.has(selfKey)) blockingOperation = operation;
401
+ else if (this.activeMaintenanceJobs.has(this._librarianKey(entityId))) blockingOperation = "librarian";
402
+ else if (this.activeMaintenanceJobs.has(this._healKey(entityId))) blockingOperation = "heal";
403
+ else if (this.activeMaintenanceJobs.has(this._pruneKey(entityId))) blockingOperation = "prune";
404
+ else if (this._isReembedActive(entityId)) blockingOperation = "reembed";
405
+ else if (this._isIngestActiveFor(entityId)) blockingOperation = "ingest";
406
+ else if (this._isImportActiveFor(entityId)) blockingOperation = "import";
407
+ else if (this._isForgetActiveFor(entityId)) blockingOperation = "forget";
408
+ break;
409
+ }
410
+ case "global_import":
411
+ if (this.activeMaintenanceJobs.has(this._globalImportKey())) blockingOperation = "import";
412
+ break;
413
+ case "ingest": {
414
+ const sourceKey = sourceRef ?? "";
415
+ if (this._hasIngestJob(entityId, sourceKey)) blockingOperation = "ingest";
416
+ else if (this.activeMaintenanceJobs.has(this._pruneKey(entityId))) blockingOperation = "prune";
417
+ else if (this._isReembedActive(entityId)) blockingOperation = "reembed";
418
+ else if (this._isImportActiveFor(entityId)) blockingOperation = "import";
419
+ else if (this._isForgetActiveFor(entityId)) blockingOperation = "forget";
420
+ break;
421
+ }
422
+ }
423
+ if (blockingOperation) {
424
+ throw new WikiBusyError(
425
+ blockingOperation,
426
+ operation === "global_reembed" || operation === "global_import" ? "*" : entityId
427
+ );
428
+ }
429
+ if (operation === "ingest") {
430
+ this._addIngestJob(entityId, sourceRef);
431
+ } else if (operation === "global_reembed") {
432
+ this.activeMaintenanceJobs.add(this._globalReembedKey());
433
+ } else if (operation === "global_import") {
434
+ this.activeMaintenanceJobs.add(this._globalImportKey());
435
+ } else {
436
+ const keyFnName = `_${operation}Key`;
437
+ const keyFn = this[keyFnName];
438
+ this.activeMaintenanceJobs.add(keyFn.call(this, entityId));
439
+ }
440
+ this._notifyStatusSubscribers(entityId);
441
+ }
442
+ releaseLock(operation, entityId, sourceRef) {
443
+ if (operation === "ingest") {
444
+ this._removeIngestJob(entityId, sourceRef);
445
+ } else if (operation === "global_reembed") {
446
+ this.activeMaintenanceJobs.delete(this._globalReembedKey());
447
+ } else if (operation === "global_import") {
448
+ this.activeMaintenanceJobs.delete(this._globalImportKey());
449
+ } else {
450
+ const keyFnName = `_${operation}Key`;
451
+ const keyFn = this[keyFnName];
452
+ this.activeMaintenanceJobs.delete(keyFn.call(this, entityId));
453
+ }
454
+ this._notifyStatusSubscribers(entityId);
455
+ }
456
+ /**
457
+ * Returns true if acquireLock(operation, entityId) would throw WikiBusyError.
458
+ * Use for non-throwing conflict checks (e.g. auto-trigger gating in write()).
459
+ */
460
+ isBlocked(operation, entityId) {
461
+ if (operation !== "global_import" && this.activeMaintenanceJobs.has(this._globalImportKey())) return true;
462
+ switch (operation) {
463
+ case "librarian":
464
+ return this.activeMaintenanceJobs.has(this._librarianKey(entityId)) || this.activeMaintenanceJobs.has(this._pruneKey(entityId)) || this._isReembedActive(entityId) || this._isImportActiveFor(entityId) || this._isForgetActiveFor(entityId);
465
+ case "heal":
466
+ return this.activeMaintenanceJobs.has(this._healKey(entityId)) || this.activeMaintenanceJobs.has(this._pruneKey(entityId)) || this._isReembedActive(entityId) || this._isImportActiveFor(entityId) || this._isForgetActiveFor(entityId);
467
+ case "prune":
468
+ return this.activeMaintenanceJobs.has(this._pruneKey(entityId)) || this.activeMaintenanceJobs.has(this._librarianKey(entityId)) || this.activeMaintenanceJobs.has(this._healKey(entityId)) || this._isReembedActive(entityId) || this._isIngestActiveFor(entityId) || this._isImportActiveFor(entityId) || this._isForgetActiveFor(entityId);
469
+ default:
470
+ return false;
471
+ }
472
+ }
473
+ /**
474
+ * Auto-heal historically only gated on the heal self-key. Keep that behavior
475
+ * for write() auto-trigger paths while preserving stricter checks in acquireLock().
476
+ */
477
+ tryAcquireAutoHealLock(entityId) {
478
+ const healKey = this._healKey(entityId);
479
+ if (this.activeMaintenanceJobs.has(healKey)) return false;
480
+ this.activeMaintenanceJobs.add(healKey);
481
+ this._notifyStatusSubscribers(entityId);
482
+ return true;
483
+ }
484
+ /**
485
+ * Validates then acquires global + per-entity import locks atomically.
486
+ * Validates all entities before acquiring any lock (same as current importDump semantics).
487
+ */
488
+ acquireImportLocks(entityIds) {
489
+ for (const entityId of entityIds) {
490
+ if (this.activeMaintenanceJobs.has(this._importKey(entityId))) throw new WikiBusyError("import", entityId);
491
+ if (this.activeMaintenanceJobs.has(this._librarianKey(entityId))) throw new WikiBusyError("librarian", entityId);
492
+ if (this.activeMaintenanceJobs.has(this._healKey(entityId))) throw new WikiBusyError("heal", entityId);
493
+ if (this.activeMaintenanceJobs.has(this._pruneKey(entityId))) throw new WikiBusyError("prune", entityId);
494
+ if (this._isReembedActive(entityId)) throw new WikiBusyError("reembed", entityId);
495
+ if (this._isIngestActiveFor(entityId)) throw new WikiBusyError("ingest", entityId);
496
+ if (this._isForgetActiveFor(entityId)) throw new WikiBusyError("forget", entityId);
497
+ }
498
+ if (this.activeMaintenanceJobs.has(this._globalImportKey())) throw new WikiBusyError("import", "*");
499
+ this.activeMaintenanceJobs.add(this._globalImportKey());
500
+ for (const entityId of entityIds) {
501
+ this.activeMaintenanceJobs.add(this._importKey(entityId));
502
+ }
503
+ }
504
+ releaseImportLocks(entityIds) {
505
+ this.activeMaintenanceJobs.delete(this._globalImportKey());
506
+ for (const entityId of entityIds) {
507
+ this.activeMaintenanceJobs.delete(this._importKey(entityId));
508
+ }
509
+ }
510
+ getEntityStatus(entityId) {
511
+ return {
512
+ ingesting: this._isIngestActiveFor(entityId),
513
+ librarian: this.activeMaintenanceJobs.has(this._librarianKey(entityId)),
514
+ heal: this.activeMaintenanceJobs.has(this._healKey(entityId))
515
+ };
516
+ }
517
+ subscribeEntityStatus(entityId, callback) {
518
+ const initial = this.getEntityStatus(entityId);
519
+ let set = this.statusSubscribers.get(entityId);
520
+ if (!set) {
521
+ set = /* @__PURE__ */ new Set();
522
+ this.statusSubscribers.set(entityId, set);
523
+ }
524
+ const entry = { callback, last: this._copyEntityStatus(initial) };
525
+ set.add(entry);
526
+ try {
527
+ callback(this._copyEntityStatus(initial));
528
+ } catch (err) {
529
+ console.error(`[JobManager] callback error for entityId="${entityId}" during initial emission`, err);
530
+ }
531
+ let active = true;
532
+ return () => {
533
+ if (!active) return;
534
+ active = false;
535
+ const s = this.statusSubscribers.get(entityId);
536
+ if (!s) return;
537
+ s.delete(entry);
538
+ if (s.size === 0) this.statusSubscribers.delete(entityId);
539
+ };
540
+ }
541
+ _copyEntityStatus(s) {
542
+ return { ingesting: s.ingesting, librarian: s.librarian, heal: s.heal };
543
+ }
544
+ _notifyStatusSubscribers(entityId) {
545
+ if (entityId === "*") return;
546
+ const set = this.statusSubscribers.get(entityId);
547
+ if (!set || set.size === 0) return;
548
+ for (const entry of Array.from(set)) {
549
+ if (!set.has(entry)) continue;
550
+ const next = this.getEntityStatus(entityId);
551
+ if (entry.last.ingesting === next.ingesting && entry.last.librarian === next.librarian && entry.last.heal === next.heal) {
552
+ continue;
553
+ }
554
+ entry.last = this._copyEntityStatus(next);
555
+ try {
556
+ entry.callback(this._copyEntityStatus(next));
557
+ } catch (err) {
558
+ console.error(`[JobManager] callback error for entityId="${entityId}" during transition emission`, err);
559
+ }
560
+ }
561
+ }
562
+ };
563
+
564
+ // src/prompts.ts
565
+ var LIBRARIAN_SYSTEM_PROMPT = `You are a knowledge extraction agent. Your job is to analyze recent episodic events and extract stable facts and actionable tasks about the user or entity.
566
+ Return ONLY a valid JSON object matching this schema:
567
+ {
568
+ "facts": [{ "title": "string (max 80 chars)", "body": "string (max 800 chars)", "tags": ["string"], "confidence": "certain|inferred|tentative" }],
569
+ "tasks": [{ "description": "string", "priority": "number (0-10)" }]
570
+ }
571
+ Keep facts concise. Do not return markdown, just raw JSON.`;
572
+ var HEAL_SYSTEM_PROMPT = `You are a memory grooming agent. Your job is to review a full dump of facts and recent events to resolve contradictions, downgrade stale claims, and flag obsolete facts for deletion.
573
+ Return ONLY a valid JSON object matching this schema:
574
+ {
575
+ "downgraded": ["string (fact IDs)"],
576
+ "deleted": ["string (fact IDs)"],
577
+ "newFacts": [{ "title": "string (max 80 chars)", "body": "string (max 800 chars)", "tags": ["string"], "confidence": "certain|inferred|tentative" }]
578
+ }
579
+ Do not return markdown, just raw JSON.`;
580
+ var INGEST_SYSTEM_PROMPT = `You are a document ingestion agent. Your job is to extract factual knowledge from the provided document chunk.
581
+ Return ONLY a valid JSON object matching this schema:
582
+ {
583
+ "facts": [{ "title": "string (max 80 chars)", "body": "string (max 800 chars)", "tags": ["string"], "confidence": "certain|inferred|tentative" }]
584
+ }
585
+ Extract verbatim factual content. Do not return markdown, just raw JSON.`;
586
+
587
+ // src/services/PromptService.ts
588
+ var PromptService = class {
589
+ constructor(globalOverrides) {
590
+ this.globalOverrides = globalOverrides;
591
+ }
592
+ hydrate(template, variables) {
593
+ return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (_match, key) => {
594
+ const value = variables[key];
595
+ if (value === void 0) return _match;
596
+ return typeof value === "string" ? value : JSON.stringify(value, null, 2);
597
+ });
598
+ }
599
+ buildIngestPrompt(documentChunk, runtimeOverride) {
600
+ const template = runtimeOverride ?? this.globalOverrides?.ingestSystemPrompt ?? INGEST_SYSTEM_PROMPT;
601
+ if (/\{\{\s*documentChunk\s*\}\}/.test(template)) {
602
+ return {
603
+ systemPrompt: this.hydrate(template, { documentChunk }),
604
+ userPrompt: "Please extract the facts."
605
+ };
606
+ }
607
+ return {
608
+ systemPrompt: template,
609
+ userPrompt: `Document Chunk:
610
+ ${documentChunk}`
611
+ };
612
+ }
613
+ buildLibrarianPrompt(events, currentFacts, runtimeOverride) {
614
+ const template = runtimeOverride ?? this.globalOverrides?.librarianSystemPrompt ?? LIBRARIAN_SYSTEM_PROMPT;
615
+ if (/\{\{\s*events\s*\}\}/.test(template) || /\{\{\s*currentFacts\s*\}\}/.test(template)) {
616
+ return {
617
+ systemPrompt: this.hydrate(template, { events, currentFacts }),
618
+ userPrompt: "Please synthesize the context."
619
+ };
620
+ }
621
+ return {
622
+ systemPrompt: template,
623
+ userPrompt: `Events:
624
+ ${JSON.stringify(events, null, 2)}
625
+
626
+ Current Facts:
627
+ ${JSON.stringify(currentFacts, null, 2)}`
628
+ };
629
+ }
630
+ buildHealPrompt(healCandidates, documentAnchors, allTasks, recentEvents, runtimeOverride) {
631
+ const template = runtimeOverride ?? this.globalOverrides?.healSystemPrompt ?? HEAL_SYSTEM_PROMPT;
632
+ if (/\{\{\s*healCandidates\s*\}\}/.test(template) || /\{\{\s*documentAnchors\s*\}\}/.test(template) || /\{\{\s*allTasks\s*\}\}/.test(template) || /\{\{\s*recentEvents\s*\}\}/.test(template)) {
633
+ return {
634
+ systemPrompt: this.hydrate(template, { healCandidates, documentAnchors, allTasks, recentEvents }),
635
+ userPrompt: "Please heal the memory graph."
636
+ };
637
+ }
638
+ return {
639
+ systemPrompt: template,
640
+ userPrompt: `Heal Candidates:
641
+ ${JSON.stringify(healCandidates, null, 2)}
642
+ Document Anchors (DO NOT MODIFY OR DELETE):
643
+ ${JSON.stringify(documentAnchors, null, 2)}
644
+ All Tasks:
645
+ ${JSON.stringify(allTasks, null, 2)}
646
+ Recent Events:
647
+ ${JSON.stringify(recentEvents, null, 2)}
648
+ The following document anchors are provided for contradiction detection only. Do not include them in \`downgraded\`, \`deleted\`, or \`newFacts\`.`
649
+ };
650
+ }
651
+ };
652
+
653
+ // src/utils/pure.ts
654
+ function parseJsonResponse(text) {
655
+ const firstBrace = text.indexOf("{");
656
+ const firstBracket = text.indexOf("[");
657
+ let start;
658
+ let openChar;
659
+ let closeChar;
660
+ const useBrace = firstBrace !== -1 && (firstBracket === -1 || firstBrace < firstBracket);
661
+ if (useBrace) {
662
+ start = firstBrace;
663
+ openChar = "{";
664
+ closeChar = "}";
665
+ } else if (firstBracket !== -1) {
666
+ start = firstBracket;
667
+ openChar = "[";
668
+ closeChar = "]";
669
+ } else {
670
+ throw new SyntaxError("No JSON object/array found in LLM response");
671
+ }
672
+ let depth = 0;
673
+ let inString = false;
674
+ let escape = false;
675
+ let end = -1;
676
+ for (let i = start; i < text.length; i++) {
677
+ const ch = text[i];
678
+ if (escape) {
679
+ escape = false;
680
+ continue;
681
+ }
682
+ if (ch === "\\" && inString) {
683
+ escape = true;
684
+ continue;
685
+ }
686
+ if (ch === '"') {
687
+ inString = !inString;
688
+ continue;
689
+ }
690
+ if (inString) continue;
691
+ if (ch === openChar) {
692
+ depth++;
693
+ continue;
694
+ }
695
+ if (ch === closeChar) {
696
+ depth--;
697
+ if (depth === 0) {
698
+ end = i;
699
+ break;
700
+ }
701
+ }
702
+ }
703
+ if (end === -1) throw new SyntaxError("No JSON object/array found in LLM response");
704
+ return JSON.parse(text.slice(start, end + 1));
705
+ }
706
+ function sanitizeRankerError(err, sanitizeRankerErrors) {
707
+ if (sanitizeRankerErrors === false) {
708
+ return err instanceof Error ? err : new Error(String(err));
709
+ }
710
+ const typeName = err instanceof Error ? err.constructor?.name ?? "Error" : typeof err;
711
+ const innerCause = err instanceof Error && err.cause !== void 0 ? new Error(`Caused by: ${err.cause?.constructor?.name ?? typeof err.cause}`) : void 0;
712
+ const sanitized = new Error(
713
+ `VectorRanker ${typeName} (message scrubbed for security)`,
714
+ innerCause ? { cause: innerCause } : void 0
715
+ );
716
+ sanitized.name = typeName;
717
+ return sanitized;
718
+ }
719
+ function safeSlice(value, start, end) {
720
+ const length = value.length;
721
+ let safeStart = start < 0 ? Math.max(length + start, 0) : Math.min(start, length);
722
+ let safeEnd = end === void 0 ? length : end < 0 ? Math.max(length + end, 0) : Math.min(end, length);
723
+ if (safeStart > safeEnd) {
724
+ [safeStart, safeEnd] = [safeEnd, safeStart];
725
+ }
726
+ if (safeStart > 0 && safeStart < length && value.charCodeAt(safeStart) >= 56320 && value.charCodeAt(safeStart) <= 57343 && value.charCodeAt(safeStart - 1) >= 55296 && value.charCodeAt(safeStart - 1) <= 56319) {
727
+ safeStart--;
728
+ }
729
+ if (safeEnd > 0 && safeEnd < length && value.charCodeAt(safeEnd - 1) >= 55296 && value.charCodeAt(safeEnd - 1) <= 56319 && value.charCodeAt(safeEnd) >= 56320 && value.charCodeAt(safeEnd) <= 57343) {
730
+ safeEnd--;
731
+ }
732
+ return value.slice(safeStart, safeEnd);
733
+ }
734
+ function chunkText(input, maxChunkLength, overlap) {
735
+ const text = input.trim();
736
+ if (text.length === 0) return { chunks: [], truncated: false };
737
+ if (!Number.isInteger(maxChunkLength) || maxChunkLength < 2) {
738
+ throw new Error("maxChunkLength must be an integer >= 2");
739
+ }
740
+ if (!Number.isInteger(overlap) || overlap < 0 || overlap >= maxChunkLength) {
741
+ throw new Error("overlap must be a non-negative integer < maxChunkLength");
742
+ }
743
+ const chunks = [];
744
+ let truncated = false;
745
+ let cursor = 0;
746
+ const halfMax = Math.floor(maxChunkLength / 2);
747
+ while (cursor < text.length) {
748
+ const remaining = text.length - cursor;
749
+ if (remaining <= maxChunkLength) {
750
+ chunks.push(safeSlice(text, cursor, text.length));
751
+ break;
752
+ }
753
+ const windowEnd = cursor + maxChunkLength;
754
+ const minSplit = cursor + halfMax;
755
+ let splitPoint = -1;
756
+ const paraIdx = text.lastIndexOf("\n\n", windowEnd);
757
+ if (paraIdx >= minSplit && paraIdx + 2 <= windowEnd) {
758
+ splitPoint = paraIdx + 2;
759
+ }
760
+ if (splitPoint === -1) {
761
+ let lastTerm = -1;
762
+ for (let i = minSplit; i < windowEnd - 1; i++) {
763
+ const ch = text[i];
764
+ if ((ch === "." || ch === "!" || ch === "?") && /\s/.test(text[i + 1])) {
765
+ lastTerm = i + 2;
766
+ }
767
+ }
768
+ if (lastTerm !== -1 && lastTerm <= windowEnd) splitPoint = lastTerm;
769
+ }
770
+ if (splitPoint === -1) {
771
+ for (let i = windowEnd - 1; i >= minSplit; i--) {
772
+ if (/\s/.test(text[i])) {
773
+ splitPoint = i + 1;
774
+ break;
775
+ }
776
+ }
777
+ }
778
+ if (splitPoint === -1) {
779
+ truncated = true;
780
+ splitPoint = windowEnd;
781
+ }
782
+ chunks.push(safeSlice(text, cursor, splitPoint));
783
+ const next = Math.max(splitPoint - overlap, cursor + 1);
784
+ cursor = next;
785
+ }
786
+ return { chunks, truncated };
787
+ }
788
+ async function withConcurrency(tasks, limit) {
789
+ const results = new Array(tasks.length);
790
+ let index = 0;
791
+ let failed = false;
792
+ let firstError;
793
+ async function worker() {
794
+ while (index < tasks.length && !failed) {
795
+ const i = index++;
796
+ try {
797
+ results[i] = await tasks[i]();
798
+ } catch (e) {
799
+ if (!failed) {
800
+ failed = true;
801
+ firstError = e;
802
+ }
803
+ return;
804
+ }
805
+ }
806
+ }
807
+ const workerCount = tasks.length === 0 ? 0 : Math.min(Math.max(limit, 1), tasks.length);
808
+ await Promise.allSettled(Array.from({ length: workerCount }, worker));
809
+ if (failed) throw firstError;
810
+ return results;
811
+ }
812
+ function clip(value, max) {
813
+ if (typeof value !== "string") return "";
814
+ const s = value.trim();
815
+ return s.length <= max ? s : safeSlice(s, 0, max).trimEnd();
816
+ }
817
+ function validateTags(tags) {
818
+ if (!Array.isArray(tags)) return [];
819
+ return tags.filter((t) => typeof t === "string").map((t) => t.trim().toLowerCase()).filter((t) => t.length > 0 && t.length <= 40).slice(0, 6);
820
+ }
821
+ function validateFact(fact) {
822
+ if (typeof fact?.title !== "string" || typeof fact?.body !== "string") return null;
823
+ const title = clip(fact.title, 80);
824
+ const body = clip(fact.body, 800);
825
+ if (!title || !body) return null;
826
+ let confidence = fact.confidence;
827
+ if (confidence !== "certain" && confidence !== "tentative") confidence = "inferred";
828
+ return {
829
+ ...fact,
830
+ title,
831
+ body,
832
+ confidence,
833
+ tags: validateTags(fact.tags)
834
+ };
835
+ }
836
+ function validateTask(task) {
837
+ if (typeof task?.description !== "string") return null;
838
+ const description = clip(task.description, 200);
839
+ if (!description) return null;
840
+ let priority = task.priority;
841
+ if (typeof priority !== "number" || !isFinite(priority)) priority = 0;
842
+ priority = Math.max(0, Math.min(10, Math.round(priority)));
843
+ return {
844
+ ...task,
845
+ description,
846
+ priority
847
+ };
848
+ }
849
+ function normalizeSourceRef(value) {
850
+ if (typeof value !== "string") return null;
851
+ const cleaned = value.replace(/[^A-Za-z0-9._\- ]/g, "").trim().slice(0, 255);
852
+ return cleaned.length > 0 ? cleaned : null;
853
+ }
854
+ function normalizeSourceHash(value) {
855
+ if (typeof value !== "string") return null;
856
+ return /^[0-9a-f]{64}$/i.test(value) ? value.toLowerCase() : null;
857
+ }
858
+ function titleTokens(title) {
859
+ return new Set(title.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((t) => t.length >= 3));
860
+ }
861
+ function jaccardScore(a, b) {
862
+ if (a.size === 0 && b.size === 0) return 0;
863
+ const intersection = new Set([...a].filter((x) => b.has(x)));
864
+ const union = /* @__PURE__ */ new Set([...a, ...b]);
865
+ return intersection.size / union.size;
866
+ }
867
+
868
+ // src/utils/ids.ts
869
+ function generateId(prefix = "") {
870
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
871
+ return prefix + crypto.randomUUID().replace(/-/g, "").substring(0, 24);
872
+ }
873
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
874
+ const bytes = new Uint8Array(16);
875
+ crypto.getRandomValues(bytes);
876
+ return prefix + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("").substring(0, 24);
877
+ }
878
+ return prefix + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
879
+ }
880
+
881
+ // src/services/IngestionService.ts
882
+ var IngestionService = class {
883
+ constructor(db, prefix, options, entryRepo, searchService, jobManager, embeddingService, promptService) {
884
+ this.db = db;
885
+ this.prefix = prefix;
886
+ this.options = options;
887
+ this.entryRepo = entryRepo;
888
+ this.searchService = searchService;
889
+ this.jobManager = jobManager;
890
+ this.embeddingService = embeddingService;
891
+ this.promptService = promptService ?? new PromptService(this.options.config?.prompts);
892
+ }
893
+ async ingestDocument(entityId, params) {
894
+ const sourceRef = normalizeSourceRef(params.sourceRef);
895
+ if (!sourceRef) throw new Error("Invalid sourceRef");
896
+ const sourceHash = normalizeSourceHash(params.sourceHash);
897
+ if (!sourceHash) throw new Error("Invalid sourceHash (must be 64-char hex string)");
898
+ const maxChunkLength = params.maxChunkLength ?? this.options.config?.maxChunkLength ?? 12e3;
899
+ const rawOverlap = params.chunkOverlap ?? this.options.config?.chunkOverlap ?? 400;
900
+ const chunkOverlap = Math.min(
901
+ Number.isFinite(rawOverlap) && rawOverlap >= 0 ? Math.floor(rawOverlap) : 400,
902
+ maxChunkLength - 1
903
+ );
904
+ const rawConcurrency = params.chunkConcurrency ?? this.options.config?.chunkConcurrency ?? 1;
905
+ const chunkConcurrency = Number.isFinite(rawConcurrency) && rawConcurrency >= 1 ? Math.floor(rawConcurrency) : 1;
906
+ if (typeof params.documentChunk !== "string") {
907
+ throw new Error(`documentChunk must be a string, received ${typeof params.documentChunk}`);
908
+ }
909
+ this.jobManager.acquireLock("ingest", entityId, sourceRef);
910
+ try {
911
+ const { chunks, truncated } = chunkText(params.documentChunk, maxChunkLength, chunkOverlap);
912
+ if (chunks.length === 0) return { truncated: false, chunks: 0 };
913
+ const chunkResults = await withConcurrency(
914
+ chunks.map((chunk) => async () => {
915
+ const { systemPrompt, userPrompt } = this.promptService.buildIngestPrompt(chunk, params.promptOverride);
916
+ const responseText = await this.options.llmProvider.generateText({ systemPrompt, userPrompt });
917
+ const result = parseJsonResponse(responseText);
918
+ return (Array.isArray(result.facts) ? result.facts : []).map(validateFact).filter((f) => f !== null);
919
+ }),
920
+ chunkConcurrency
921
+ );
922
+ const seen = /* @__PURE__ */ new Set();
923
+ const allValidFacts = [];
924
+ for (const facts of chunkResults) {
925
+ for (const fact of facts) {
926
+ const normalized = fact.title.trim().toLowerCase().replace(/\s+/g, " ");
927
+ if (!seen.has(normalized)) {
928
+ seen.add(normalized);
929
+ allValidFacts.push(fact);
930
+ }
931
+ }
932
+ }
933
+ const now = Date.now();
934
+ const insertedFacts = [];
935
+ const deletedSourceFactIds = [];
936
+ await this.db.withTransactionAsync(async (tx) => {
937
+ deletedSourceFactIds.push(...await this.entryRepo.findIdsBySource(entityId, sourceRef, null, tx, false));
938
+ await this.entryRepo.softDeleteBySource(entityId, tx, sourceRef, null);
939
+ for (const fact of allValidFacts) {
940
+ const id = generateId("fact_");
941
+ const wikiFact = {
942
+ id,
943
+ entity_id: entityId,
944
+ title: fact.title,
945
+ body: fact.body,
946
+ tags: fact.tags,
947
+ confidence: fact.confidence,
948
+ source_type: "immutable_document",
949
+ source_hash: sourceHash,
950
+ source_ref: sourceRef,
951
+ created_at: now,
952
+ updated_at: now,
953
+ last_accessed_at: null,
954
+ access_count: 0,
955
+ deleted_at: null
956
+ };
957
+ await this.entryRepo.upsert(wikiFact, tx);
958
+ insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
959
+ }
960
+ });
961
+ await this.searchService.sync(entityId);
962
+ const uniqueDeletedSourceFactIds = Array.from(new Set(deletedSourceFactIds));
963
+ for (const factId of uniqueDeletedSourceFactIds) {
964
+ try {
965
+ await this.embeddingService.notifyEmbeddingPersisted(entityId, factId, null);
966
+ } catch (hookErr) {
967
+ console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during ingest for ${factId}:`, hookErr);
968
+ }
969
+ }
970
+ for (const fact of insertedFacts) {
971
+ await this.embeddingService.embedFact(fact);
972
+ }
973
+ this.searchService.evictCache(entityId);
974
+ return { truncated, chunks: chunks.length };
975
+ } finally {
976
+ this.jobManager.releaseLock("ingest", entityId, sourceRef);
977
+ }
978
+ }
979
+ };
980
+
981
+ // src/services/MaintenanceService.ts
982
+ var FUZZY_THRESHOLD = 0.5;
983
+ var MIN_TOKENS_TO_QUALIFY = 3;
984
+ var MaintenanceService = class {
985
+ constructor(db, prefix, options, entryRepo, taskRepo, eventRepo, metadataRepo, searchService, jobManager, embeddingService, promptService) {
986
+ this.db = db;
987
+ this.prefix = prefix;
988
+ this.options = options;
989
+ this.entryRepo = entryRepo;
990
+ this.taskRepo = taskRepo;
991
+ this.eventRepo = eventRepo;
992
+ this.metadataRepo = metadataRepo;
993
+ this.searchService = searchService;
994
+ this.jobManager = jobManager;
995
+ this.embeddingService = embeddingService;
996
+ this.promptService = promptService ?? new PromptService(this.options.config?.prompts);
997
+ }
998
+ async runPrune(entityId, options) {
999
+ this.jobManager.acquireLock("prune", entityId);
1000
+ try {
1001
+ const retainSoftDeletedFor = options?.retainSoftDeletedFor !== void 0 ? options.retainSoftDeletedFor : this.options.config?.pruneRetainSoftDeletedFor ?? 7;
1002
+ const retainEventsFor = options?.retainEventsFor !== void 0 ? options.retainEventsFor : this.options.config?.pruneEventsAfter ?? 30;
1003
+ const vacuum = options?.vacuum ?? false;
1004
+ this._validatePruneDuration(retainSoftDeletedFor, "retainSoftDeletedFor");
1005
+ this._validatePruneDuration(retainEventsFor, "retainEventsFor");
1006
+ const now = Date.now();
1007
+ let deletedEntries = 0;
1008
+ let deletedTasks = 0;
1009
+ let deletedEvents = 0;
1010
+ if (retainSoftDeletedFor !== null) {
1011
+ const cutoff = now - retainSoftDeletedFor * 864e5;
1012
+ const entriesToDelete = await this.entryRepo.getPrunableMetadata(entityId, cutoff);
1013
+ const succeeded = [];
1014
+ let failure = null;
1015
+ for (const row of entriesToDelete) {
1016
+ try {
1017
+ await this.embeddingService.notifyEmbeddingPersistedOrThrow(row.entity_id, row.id, null);
1018
+ succeeded.push({ entity_id: row.entity_id, id: row.id });
1019
+ } catch (err) {
1020
+ failure = { factId: row.id, cause: err };
1021
+ break;
1022
+ }
1023
+ }
1024
+ const succeededIds = succeeded.map((r) => r.id);
1025
+ await this.db.withTransactionAsync(async (tx) => {
1026
+ if (succeededIds.length > 0) {
1027
+ deletedEntries = await this.entryRepo.bulkDeletePruned(entityId, cutoff, succeededIds, tx);
1028
+ }
1029
+ deletedTasks = await this.taskRepo.bulkDeletePruned(entityId, cutoff, tx);
1030
+ });
1031
+ if (failure) {
1032
+ await this.searchService.sync(entityId);
1033
+ const remaining = entriesToDelete.length - succeeded.length - 1;
1034
+ const isTimeout = failure.cause?.[HOOK_TIMEOUT_MARKER] === true;
1035
+ if (isTimeout) {
1036
+ throw new PrunePartialFailureError(
1037
+ succeeded.length,
1038
+ failure.factId,
1039
+ remaining,
1040
+ new Error("Deletion hook timed out"),
1041
+ deletedTasks,
1042
+ 0
1043
+ );
1044
+ }
1045
+ const errMsg = failure.cause?.message ?? "";
1046
+ const isValidationError = errMsg.startsWith("Invalid deletionHookTimeoutMs");
1047
+ const sanitizedCause = isValidationError ? failure.cause : this._sanitizeRankerError(failure.cause);
1048
+ throw new PrunePartialFailureError(
1049
+ succeeded.length,
1050
+ failure.factId,
1051
+ remaining,
1052
+ sanitizedCause,
1053
+ deletedTasks,
1054
+ 0
1055
+ );
1056
+ }
1057
+ }
1058
+ if (retainEventsFor !== null) {
1059
+ const cutoff = now - retainEventsFor * 864e5;
1060
+ const eventResult = await this.eventRepo.prune(entityId, cutoff);
1061
+ deletedEvents = eventResult.changes;
1062
+ }
1063
+ if (vacuum) {
1064
+ await this.metadataRepo.vacuum();
1065
+ }
1066
+ await this.searchService.sync(entityId);
1067
+ return { entries: deletedEntries, tasks: deletedTasks, events: deletedEvents };
1068
+ } finally {
1069
+ this.jobManager.releaseLock("prune", entityId);
1070
+ }
1071
+ }
1072
+ async runLibrarian(entityId, options) {
1073
+ this.jobManager.acquireLock("librarian", entityId);
1074
+ try {
1075
+ await this.doRunLibrarian(entityId, options?.promptOverride);
1076
+ } finally {
1077
+ this.jobManager.releaseLock("librarian", entityId);
1078
+ }
1079
+ }
1080
+ async runHeal(entityId, options) {
1081
+ this.jobManager.acquireLock("heal", entityId);
1082
+ try {
1083
+ await this.doRunHeal(entityId, options?.promptOverride);
1084
+ } finally {
1085
+ this.jobManager.releaseLock("heal", entityId);
1086
+ }
1087
+ }
1088
+ async runReembed(entityId, opts) {
1089
+ const embedFn = this.options.llmProvider.embed;
1090
+ if (!embedFn) return { embedded: 0, skipped: 0, failed: 0 };
1091
+ const op = entityId ? "reembed" : "global_reembed";
1092
+ this.jobManager.acquireLock(op, entityId ?? "*");
1093
+ try {
1094
+ const rows = await this.entryRepo.findAllForReembed(entityId);
1095
+ this.searchService.evictCache(entityId);
1096
+ const skipExisting = opts?.skipExisting ?? false;
1097
+ let effectiveSkip = skipExisting;
1098
+ if (skipExisting) {
1099
+ const mismatchValue = await this.metadataRepo.getMeta("embedding_dimension_mismatch");
1100
+ if (mismatchValue) {
1101
+ if (entityId) {
1102
+ const mismatchDim = parseInt(mismatchValue, 10);
1103
+ const staleCount = await this.entryRepo.countStaleForEntity(entityId, mismatchDim);
1104
+ if (staleCount > 0) effectiveSkip = false;
1105
+ } else {
1106
+ effectiveSkip = false;
1107
+ }
1108
+ }
1109
+ }
1110
+ let embedded = 0;
1111
+ let skipped = 0;
1112
+ let failed = 0;
1113
+ try {
1114
+ for (const row of rows) {
1115
+ const existingBlob = row.embedding_blob;
1116
+ const blobIsValid = !!existingBlob && existingBlob.byteLength > 0 && existingBlob.byteLength % 4 === 0;
1117
+ if (effectiveSkip && blobIsValid) {
1118
+ const vec = parseEmbedding(existingBlob, null);
1119
+ if (vec !== null && vec.every((v) => Number.isFinite(v))) {
1120
+ skipped++;
1121
+ continue;
1122
+ }
1123
+ }
1124
+ const success = await this.embeddingService.embedFact(row);
1125
+ if (success) embedded++;
1126
+ else failed++;
1127
+ }
1128
+ if (embedded > 0) {
1129
+ await this.embeddingService.reconcileEmbeddingDimension();
1130
+ }
1131
+ } finally {
1132
+ this.searchService.evictCache(entityId);
1133
+ }
1134
+ return { embedded, skipped, failed };
1135
+ } finally {
1136
+ this.jobManager.releaseLock(op, entityId ?? "*");
1137
+ }
1138
+ }
1139
+ async forget(entityId, params) {
1140
+ if (params.clearAll && (params.entryId !== void 0 || params.taskId !== void 0 || params.sourceRef !== void 0 || params.sourceHash !== void 0)) {
1141
+ throw new Error("forget() clearAll is mutually exclusive with entryId, taskId, sourceRef, and sourceHash");
1142
+ }
1143
+ this.jobManager.acquireLock("forget", entityId);
1144
+ try {
1145
+ const now = Date.now();
1146
+ let deletedEntries = 0;
1147
+ let deletedTasks = 0;
1148
+ const deletedEntryIds = [];
1149
+ await this.db.withTransactionAsync(async (tx) => {
1150
+ if (params.clearAll) {
1151
+ deletedEntryIds.push(...await this.entryRepo.findIdsBySource(entityId, null, null, tx, true));
1152
+ deletedEntries = await this.entryRepo.bulkSoftDeleteByEntityId(entityId, tx);
1153
+ deletedTasks = await this.taskRepo.bulkSoftDeleteByEntityId(entityId, tx);
1154
+ await this.metadataRepo.updateCheckpoint(entityId, { memory: 0, heal: 0 }, tx);
1155
+ } else {
1156
+ const hasIdSelectors = params.entryId !== void 0 || params.taskId !== void 0;
1157
+ const hasSourceSelectors = params.sourceRef !== void 0 || params.sourceHash !== void 0;
1158
+ if (hasIdSelectors && hasSourceSelectors) {
1159
+ throw new Error("forget() params are mutually exclusive: use entryId/taskId together, or sourceRef/sourceHash together, but not both in the same call");
1160
+ }
1161
+ const sourceRef = params.sourceRef !== void 0 ? normalizeSourceRef(params.sourceRef) : null;
1162
+ if (params.sourceRef !== void 0 && !sourceRef) throw new Error("Invalid sourceRef");
1163
+ const sourceHash = params.sourceHash !== void 0 ? normalizeSourceHash(params.sourceHash) : null;
1164
+ if (params.sourceHash !== void 0 && !sourceHash) throw new Error("Invalid sourceHash (must be 64-char hex string)");
1165
+ if (params.entryId) {
1166
+ const entryId = await this.entryRepo.findIdById(params.entryId, entityId, tx);
1167
+ if (entryId) deletedEntryIds.push(entryId);
1168
+ }
1169
+ if (sourceRef || sourceHash) {
1170
+ deletedEntryIds.push(...await this.entryRepo.findIdsBySource(entityId, sourceRef, sourceHash, tx, true));
1171
+ }
1172
+ const entryPromise = params.entryId ? this.entryRepo.softDelete(params.entryId, entityId, tx).then((r) => r.changes > 0) : null;
1173
+ const taskDeletedPromise = params.taskId ? this.taskRepo.softDeleteById(params.taskId, entityId, tx).then((r) => r.changes > 0) : null;
1174
+ const refPromise = sourceRef || sourceHash ? this.entryRepo.softDeleteBySource(entityId, tx, sourceRef, sourceHash) : null;
1175
+ const [entryResult, taskResult, refResult] = await Promise.all([
1176
+ entryPromise ?? Promise.resolve(false),
1177
+ taskDeletedPromise ?? Promise.resolve(false),
1178
+ refPromise ?? Promise.resolve(0)
1179
+ ]);
1180
+ if (entryResult) deletedEntries++;
1181
+ if (taskResult) deletedTasks++;
1182
+ deletedEntries += refResult;
1183
+ }
1184
+ });
1185
+ await this.searchService.sync(entityId);
1186
+ const uniqueDeletedIds = Array.from(new Set(deletedEntryIds));
1187
+ for (const factId of uniqueDeletedIds) {
1188
+ try {
1189
+ await this.embeddingService.notifyEmbeddingPersistedOrThrow(entityId, factId, null);
1190
+ } catch (hookErr) {
1191
+ const isTimeout = hookErr?.[HOOK_TIMEOUT_MARKER] === true;
1192
+ if (isTimeout) {
1193
+ throw new Error(`forget(${entityId}/${factId}) failed: ${hookErr.message}`);
1194
+ }
1195
+ const errMsg = hookErr?.message ?? "";
1196
+ if (errMsg.startsWith("Invalid deletionHookTimeoutMs")) {
1197
+ throw new Error(`forget(${entityId}/${factId}) failed: ${errMsg}`, { cause: hookErr });
1198
+ }
1199
+ throw new Error(`forget(${entityId}/${factId}) failed: ANN cleanup hook rejected`, { cause: this._sanitizeRankerError(hookErr) });
1200
+ }
1201
+ }
1202
+ return { deleted: { entries: deletedEntries, tasks: deletedTasks } };
1203
+ } finally {
1204
+ this.jobManager.releaseLock("forget", entityId);
1205
+ }
1206
+ }
1207
+ /** Core librarian pass (locks handled by {@link runLibrarian}). Package-internal orchestration hook. */
1208
+ async doRunLibrarian(entityId, promptOverride) {
1209
+ const events = await this.eventRepo.getRecent(entityId, 50);
1210
+ const currentFactsRows = await this.entryRepo.findRecentByEntityId(entityId, 100);
1211
+ const currentFacts = currentFactsRows.map((f) => {
1212
+ const { embedding: _embedding, embedding_blob: _blob, ...rest } = f;
1213
+ return {
1214
+ ...rest,
1215
+ tags: typeof rest.tags === "string" ? JSON.parse(rest.tags) : rest.tags
1216
+ };
1217
+ });
1218
+ const { systemPrompt, userPrompt } = this.promptService.buildLibrarianPrompt(
1219
+ events.reverse(),
1220
+ currentFacts,
1221
+ promptOverride
1222
+ );
1223
+ const responseText = await this.options.llmProvider.generateText({ systemPrompt, userPrompt });
1224
+ const result = parseJsonResponse(responseText);
1225
+ const facts = Array.isArray(result.facts) ? result.facts : [];
1226
+ const tasks = Array.isArray(result.tasks) ? result.tasks : [];
1227
+ const validFacts = facts.map(validateFact).filter((f) => f !== null);
1228
+ const validTasks = tasks.map(validateTask).filter((t) => t !== null);
1229
+ const now = Date.now();
1230
+ const insertedFacts = [];
1231
+ await this.db.withTransactionAsync(async (tx) => {
1232
+ const factsForDedupe = await this.entryRepo.findRecentByEntityId(entityId, 100, tx);
1233
+ for (const fact of validFacts) {
1234
+ const newTokens = titleTokens(fact.title);
1235
+ let skip = false;
1236
+ if (newTokens.size >= MIN_TOKENS_TO_QUALIFY) {
1237
+ for (const existing of factsForDedupe) {
1238
+ if (existing.source_type !== "librarian_inferred") continue;
1239
+ const existingTokens = titleTokens(existing.title);
1240
+ if (existingTokens.size >= MIN_TOKENS_TO_QUALIFY) {
1241
+ if (jaccardScore(newTokens, existingTokens) >= FUZZY_THRESHOLD) {
1242
+ skip = true;
1243
+ break;
1244
+ }
1245
+ }
1246
+ }
1247
+ }
1248
+ if (skip) continue;
1249
+ const id = generateId("fact_");
1250
+ const factObj = {
1251
+ id,
1252
+ entity_id: entityId,
1253
+ title: fact.title,
1254
+ body: fact.body,
1255
+ tags: fact.tags,
1256
+ confidence: fact.confidence,
1257
+ source_type: "librarian_inferred",
1258
+ source_hash: null,
1259
+ source_ref: null,
1260
+ created_at: now,
1261
+ updated_at: now,
1262
+ last_accessed_at: null,
1263
+ access_count: 0,
1264
+ deleted_at: null
1265
+ };
1266
+ await this.entryRepo.upsert(factObj, tx);
1267
+ insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
1268
+ factsForDedupe.push(factObj);
1269
+ }
1270
+ for (const task of validTasks) {
1271
+ const id = generateId("task_");
1272
+ const taskObj = {
1273
+ id,
1274
+ entity_id: entityId,
1275
+ description: task.description,
1276
+ status: "pending",
1277
+ priority: task.priority,
1278
+ created_at: now,
1279
+ updated_at: now,
1280
+ resolved_at: null,
1281
+ deleted_at: null
1282
+ };
1283
+ await this.taskRepo.upsert(taskObj, tx);
1284
+ }
1285
+ });
1286
+ await this.searchService.sync(entityId);
1287
+ for (const fact of insertedFacts) {
1288
+ await this.embeddingService.embedFact(fact);
1289
+ }
1290
+ this.searchService.evictCache(entityId);
1291
+ }
1292
+ /** Core heal pass (locks handled by {@link runHeal}). Package-internal orchestration hook. */
1293
+ async doRunHeal(entityId, promptOverride) {
1294
+ const now = Date.now();
1295
+ const orphanAfterDays = this.options.config?.orphanAfterDays !== void 0 ? this.options.config?.orphanAfterDays : 30;
1296
+ const staleInferredAfterDays = this.options.config?.staleInferredAfterDays !== void 0 ? this.options.config?.staleInferredAfterDays : 60;
1297
+ const MS_PER_DAY = 24 * 60 * 60 * 1e3;
1298
+ if (orphanAfterDays !== null && (typeof orphanAfterDays !== "number" || !Number.isFinite(orphanAfterDays) || orphanAfterDays < 0)) {
1299
+ throw new Error("Invalid orphanAfterDays: must be a finite number >= 0 or null");
1300
+ }
1301
+ if (staleInferredAfterDays !== null && (typeof staleInferredAfterDays !== "number" || !Number.isFinite(staleInferredAfterDays) || staleInferredAfterDays < 0)) {
1302
+ throw new Error("Invalid staleInferredAfterDays: must be a finite number >= 0 or null");
1303
+ }
1304
+ const orphanedIds = [];
1305
+ await this.db.withTransactionAsync(async (tx) => {
1306
+ if (orphanAfterDays !== null) {
1307
+ const orphanThreshold = now - orphanAfterDays * MS_PER_DAY;
1308
+ orphanedIds.push(...await this.entryRepo.markOrphaned(entityId, orphanThreshold, tx));
1309
+ }
1310
+ if (staleInferredAfterDays !== null) {
1311
+ const staleThreshold = now - staleInferredAfterDays * MS_PER_DAY;
1312
+ await this.entryRepo.downgradeStaleInferred(entityId, staleThreshold, tx);
1313
+ }
1314
+ });
1315
+ for (const factId of orphanedIds) {
1316
+ try {
1317
+ await this.embeddingService.notifyEmbeddingPersisted(entityId, factId, null);
1318
+ } catch (hookErr) {
1319
+ console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during heal orphan pass for ${factId}:`, hookErr);
1320
+ }
1321
+ }
1322
+ const allFactsRows = await this.entryRepo.findAllByEntityId(entityId);
1323
+ const allTasks = await this.taskRepo.findAllPending([entityId]);
1324
+ const recentEvents = await this.eventRepo.getRecent(entityId, 20);
1325
+ const healCandidates = allFactsRows.filter((f) => f.source_type !== "immutable_document");
1326
+ const documentAnchors = allFactsRows.filter((f) => f.source_type === "immutable_document").map(({ id, title, source_ref }) => ({ id, title, source_ref }));
1327
+ const healCandidatesForPrompt = healCandidates.map((f) => {
1328
+ const { embedding: _embedding, embedding_blob: _blob, ...rest } = f;
1329
+ return { ...rest, tags: typeof rest.tags === "string" ? JSON.parse(rest.tags) : rest.tags };
1330
+ });
1331
+ const { systemPrompt, userPrompt } = this.promptService.buildHealPrompt(
1332
+ healCandidatesForPrompt,
1333
+ documentAnchors,
1334
+ allTasks,
1335
+ recentEvents,
1336
+ promptOverride
1337
+ );
1338
+ const responseText = await this.options.llmProvider.generateText({ systemPrompt, userPrompt });
1339
+ const result = parseJsonResponse(responseText);
1340
+ const mutableIds = new Set(healCandidates.map((f) => f.id));
1341
+ const downgraded = Array.isArray(result.downgraded) ? result.downgraded : [];
1342
+ const deleted = Array.isArray(result.deleted) ? result.deleted : [];
1343
+ const newFacts = Array.isArray(result.newFacts) ? result.newFacts : [];
1344
+ const safeDowngraded = Array.from(new Set(downgraded.filter((id) => mutableIds.has(id))));
1345
+ const safeDeleted = Array.from(new Set(deleted.filter((id) => mutableIds.has(id))));
1346
+ const validNewFacts = newFacts.map(validateFact).filter((f) => f !== null);
1347
+ const insertedFacts = [];
1348
+ const uniqueDeletedFactIds = Array.from(new Set(safeDeleted));
1349
+ const healFactsForDedupe = [...healCandidates];
1350
+ await this.db.withTransactionAsync(async (tx) => {
1351
+ await this.entryRepo.downgradeByIds(safeDowngraded, entityId, tx);
1352
+ await this.entryRepo.softDeleteByIds(safeDeleted, entityId, tx);
1353
+ for (const fact of validNewFacts) {
1354
+ const newTokens = titleTokens(fact.title);
1355
+ let skip = false;
1356
+ if (newTokens.size >= MIN_TOKENS_TO_QUALIFY) {
1357
+ for (const existing of healFactsForDedupe) {
1358
+ if (existing.source_type !== "librarian_inferred") continue;
1359
+ const existingTokens = titleTokens(existing.title);
1360
+ if (existingTokens.size >= MIN_TOKENS_TO_QUALIFY) {
1361
+ if (jaccardScore(newTokens, existingTokens) >= FUZZY_THRESHOLD) {
1362
+ skip = true;
1363
+ break;
1364
+ }
1365
+ }
1366
+ }
1367
+ }
1368
+ if (skip) continue;
1369
+ const id = generateId("fact_");
1370
+ const factObj = {
1371
+ id,
1372
+ entity_id: entityId,
1373
+ title: fact.title,
1374
+ body: fact.body,
1375
+ tags: fact.tags,
1376
+ confidence: fact.confidence,
1377
+ source_type: "librarian_inferred",
1378
+ source_hash: null,
1379
+ source_ref: null,
1380
+ created_at: now,
1381
+ updated_at: now,
1382
+ last_accessed_at: null,
1383
+ access_count: 0,
1384
+ deleted_at: null
1385
+ };
1386
+ await this.entryRepo.upsert(factObj, tx);
1387
+ insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
1388
+ healFactsForDedupe.push(factObj);
1389
+ }
1390
+ });
1391
+ await this.searchService.sync(entityId);
1392
+ for (const factId of uniqueDeletedFactIds) {
1393
+ try {
1394
+ await this.embeddingService.notifyEmbeddingPersisted(entityId, factId, null);
1395
+ } catch (hookErr) {
1396
+ console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during heal for ${factId}:`, hookErr);
1397
+ }
1398
+ }
1399
+ for (const fact of insertedFacts) {
1400
+ await this.embeddingService.embedFact(fact);
1401
+ }
1402
+ this.searchService.evictCache(entityId);
1403
+ }
1404
+ _validatePruneDuration(value, name) {
1405
+ if (value !== null && value !== void 0 && (typeof value !== "number" || !isFinite(value) || value < 0)) {
1406
+ throw new Error(`Invalid ${name}: must be a non-negative finite number or null`);
1407
+ }
1408
+ }
1409
+ _sanitizeRankerError(err) {
1410
+ return sanitizeRankerError(err, this.options.sanitizeRankerErrors);
1411
+ }
1412
+ };
1413
+
1414
+ // src/services/ImportExportService.ts
1415
+ var ImportExportService = class {
1416
+ constructor(db, entryRepo, taskRepo, eventRepo, metadataRepo, searchService, jobManager, embeddingService) {
1417
+ this.db = db;
1418
+ this.entryRepo = entryRepo;
1419
+ this.taskRepo = taskRepo;
1420
+ this.eventRepo = eventRepo;
1421
+ this.metadataRepo = metadataRepo;
1422
+ this.searchService = searchService;
1423
+ this.jobManager = jobManager;
1424
+ this.embeddingService = embeddingService;
1425
+ }
1426
+ async exportDump(entityIds) {
1427
+ let ids;
1428
+ if (entityIds && entityIds.length > 0) {
1429
+ ids = Array.from(new Set(entityIds));
1430
+ } else {
1431
+ ids = await this.metadataRepo.getDistinctEntityIds();
1432
+ }
1433
+ const entities = {};
1434
+ const BATCH = 3;
1435
+ for (let i = 0; i < ids.length; i += BATCH) {
1436
+ const batch = ids.slice(i, i + BATCH);
1437
+ const batchResults = await Promise.all(
1438
+ batch.map(
1439
+ async (id) => [
1440
+ id,
1441
+ await this.getFullBundle(id, { includeBlobs: true })
1442
+ ]
1443
+ )
1444
+ );
1445
+ for (const [id, bundle] of batchResults) {
1446
+ entities[id] = bundle;
1447
+ }
1448
+ }
1449
+ return { generatedAt: Date.now(), entities };
1450
+ }
1451
+ async importDump(dump, opts) {
1452
+ const merge = opts?.merge ?? false;
1453
+ const entityIds = Object.keys(dump.entities);
1454
+ this.jobManager.acquireImportLocks(entityIds);
1455
+ try {
1456
+ await this.assertNoLegacySourceTypes();
1457
+ for (const [entityId, bundle] of Object.entries(dump.entities)) {
1458
+ await this.doImportEntity(entityId, bundle, merge);
1459
+ }
1460
+ } finally {
1461
+ this.jobManager.releaseImportLocks(entityIds);
1462
+ }
1463
+ }
1464
+ async getFullBundle(entityId, opts) {
1465
+ const [factsRaw, tasks, events] = await Promise.all([
1466
+ opts?.includeBlobs ? this.entryRepo.findAllByEntityIdWithBlobs(entityId) : this.entryRepo.findAllByEntityId(entityId),
1467
+ this.taskRepo.findAllByEntityId(entityId),
1468
+ this.eventRepo.getByEntityId(entityId, opts?.maxEvents)
1469
+ ]);
1470
+ const facts = factsRaw.map((f) => {
1471
+ const {
1472
+ embedding: _embedding,
1473
+ embedding_blob,
1474
+ ...rest
1475
+ } = f;
1476
+ const safeBlobCopy = opts?.includeBlobs && embedding_blob ? (() => {
1477
+ const c = new ArrayBuffer(embedding_blob.byteLength);
1478
+ new Uint8Array(c).set(embedding_blob);
1479
+ return new Uint8Array(c);
1480
+ })() : void 0;
1481
+ const factBase = safeBlobCopy ? { ...rest, embedding_blob: safeBlobCopy } : rest;
1482
+ return {
1483
+ ...factBase,
1484
+ tags: typeof factBase.tags === "string" ? JSON.parse(factBase.tags) : factBase.tags
1485
+ };
1486
+ });
1487
+ return { facts, tasks, events };
1488
+ }
1489
+ /** Single-entity import transaction + post-processing; package-internal hook for tests. */
1490
+ async doImportEntity(entityId, bundle, merge) {
1491
+ const upsertedFactIds = /* @__PURE__ */ new Set();
1492
+ const upsertedDeletedFactIds = /* @__PURE__ */ new Set();
1493
+ const factsWithPreservedBlob = /* @__PURE__ */ new Map();
1494
+ const preservedBlobDims = /* @__PURE__ */ new Set();
1495
+ const softDeletedFactIds = [];
1496
+ await this.db.withTransactionAsync(async (tx) => {
1497
+ if (!merge) {
1498
+ const deletedLiveFactIds = await this.entryRepo.findIdsBySource(
1499
+ entityId,
1500
+ null,
1501
+ null,
1502
+ tx,
1503
+ false
1504
+ );
1505
+ softDeletedFactIds.push(...deletedLiveFactIds);
1506
+ await this.entryRepo.bulkSoftDeleteByEntityId(entityId, tx);
1507
+ await this.taskRepo.bulkSoftDeleteByEntityId(entityId, tx);
1508
+ await this.metadataRepo.deleteCheckpoint(entityId, tx);
1509
+ }
1510
+ const factIds = bundle.facts.map((fact) => fact.id);
1511
+ const existingFactsById = /* @__PURE__ */ new Map();
1512
+ const existingFacts = await this.entryRepo.findExistingMetadataByIds(
1513
+ factIds,
1514
+ tx
1515
+ );
1516
+ for (const existingFact of existingFacts) {
1517
+ existingFactsById.set(existingFact.id, existingFact);
1518
+ }
1519
+ for (const fact of bundle.facts) {
1520
+ const sourceType = this._normalizeImportedSourceType(
1521
+ String(fact.source_type),
1522
+ {
1523
+ entityId,
1524
+ factId: fact.id
1525
+ }
1526
+ );
1527
+ const safeUpdatedAt = Number.isFinite(fact.updated_at) ? fact.updated_at : 0;
1528
+ const existing = existingFactsById.get(fact.id);
1529
+ const rawBlobRaw = fact.embedding_blob;
1530
+ let rawBlob = null;
1531
+ if (rawBlobRaw instanceof Uint8Array) {
1532
+ rawBlob = rawBlobRaw;
1533
+ } else if (rawBlobRaw !== null && rawBlobRaw !== void 0 && typeof rawBlobRaw === "object") {
1534
+ const obj = rawBlobRaw;
1535
+ if (obj["type"] === "Buffer" && Array.isArray(obj["data"])) {
1536
+ rawBlob = new Uint8Array(obj["data"]);
1537
+ } else if (!Array.isArray(rawBlobRaw)) {
1538
+ const entries = Object.keys(obj);
1539
+ if (entries.length > 0 && entries.every((k) => /^\d+$/.test(k))) {
1540
+ const len = entries.length;
1541
+ rawBlob = new Uint8Array(len);
1542
+ for (let i = 0; i < len; i++)
1543
+ rawBlob[i] = obj[String(i)] ?? 0;
1544
+ }
1545
+ }
1546
+ }
1547
+ let blobData = null;
1548
+ if (rawBlob !== null && rawBlob.byteLength > 0 && rawBlob.byteLength % 4 === 0) {
1549
+ const copy = new ArrayBuffer(rawBlob.byteLength);
1550
+ const alignedBlob = new Uint8Array(copy);
1551
+ alignedBlob.set(rawBlob);
1552
+ const floats = new Float32Array(copy, 0, rawBlob.byteLength / 4);
1553
+ let allFinite = true;
1554
+ for (let i = 0; i < floats.length; i++) {
1555
+ if (!isFinite(floats[i])) {
1556
+ allFinite = false;
1557
+ break;
1558
+ }
1559
+ }
1560
+ if (allFinite) {
1561
+ blobData = alignedBlob;
1562
+ }
1563
+ }
1564
+ if (existing) {
1565
+ if (existing.entity_id !== entityId) {
1566
+ this._warnCrossEntityCollision(
1567
+ "entry",
1568
+ fact.id,
1569
+ existing.entity_id,
1570
+ entityId
1571
+ );
1572
+ continue;
1573
+ }
1574
+ if (merge && safeUpdatedAt <= existing.updated_at) continue;
1575
+ }
1576
+ const factObj = {
1577
+ id: fact.id,
1578
+ entity_id: entityId,
1579
+ title: fact.title,
1580
+ body: fact.body,
1581
+ tags: Array.isArray(fact.tags) ? fact.tags : [],
1582
+ confidence: fact.confidence,
1583
+ source_type: sourceType,
1584
+ source_hash: fact.source_hash,
1585
+ source_ref: fact.source_ref,
1586
+ created_at: fact.created_at,
1587
+ updated_at: safeUpdatedAt,
1588
+ last_accessed_at: fact.last_accessed_at,
1589
+ access_count: fact.access_count,
1590
+ deleted_at: fact.deleted_at,
1591
+ embedding_blob: blobData ?? void 0
1592
+ };
1593
+ await this.entryRepo.upsertForImport(factObj, tx);
1594
+ if (blobData != null) {
1595
+ factsWithPreservedBlob.set(fact.id, blobData);
1596
+ if (!fact.deleted_at) preservedBlobDims.add(blobData.byteLength / 4);
1597
+ }
1598
+ existingFactsById.set(fact.id, {
1599
+ id: fact.id,
1600
+ entity_id: entityId,
1601
+ updated_at: safeUpdatedAt
1602
+ });
1603
+ upsertedFactIds.add(fact.id);
1604
+ if (fact.deleted_at) upsertedDeletedFactIds.add(fact.id);
1605
+ }
1606
+ const taskIds = bundle.tasks.map((task) => task.id);
1607
+ const existingTasksById = /* @__PURE__ */ new Map();
1608
+ const existingTasks = await this.taskRepo.findExistingMetadataByIds(
1609
+ taskIds,
1610
+ tx
1611
+ );
1612
+ for (const existingTask of existingTasks) {
1613
+ existingTasksById.set(existingTask.id, existingTask);
1614
+ }
1615
+ for (const task of bundle.tasks) {
1616
+ const safeUpdatedAt = Number.isFinite(task.updated_at) ? task.updated_at : 0;
1617
+ const existing = existingTasksById.get(task.id);
1618
+ if (existing) {
1619
+ if (existing.entity_id !== entityId) {
1620
+ this._warnCrossEntityCollision(
1621
+ "task",
1622
+ task.id,
1623
+ existing.entity_id,
1624
+ entityId
1625
+ );
1626
+ continue;
1627
+ }
1628
+ if (merge && safeUpdatedAt <= existing.updated_at) continue;
1629
+ }
1630
+ await this.taskRepo.upsertForImport(
1631
+ {
1632
+ id: task.id,
1633
+ entity_id: entityId,
1634
+ description: task.description,
1635
+ status: task.status,
1636
+ priority: task.priority,
1637
+ created_at: task.created_at,
1638
+ updated_at: safeUpdatedAt,
1639
+ resolved_at: task.resolved_at,
1640
+ deleted_at: task.deleted_at
1641
+ },
1642
+ tx,
1643
+ safeUpdatedAt
1644
+ );
1645
+ existingTasksById.set(task.id, {
1646
+ id: task.id,
1647
+ entity_id: entityId,
1648
+ updated_at: safeUpdatedAt
1649
+ });
1650
+ }
1651
+ for (const event of bundle.events) {
1652
+ await this.eventRepo.addIgnoreDuplicate(
1653
+ {
1654
+ id: event.id,
1655
+ entity_id: entityId,
1656
+ event_type: event.event_type,
1657
+ summary: event.summary,
1658
+ related_entry_id: event.related_entry_id ?? null,
1659
+ created_at: event.created_at
1660
+ },
1661
+ tx
1662
+ );
1663
+ }
1664
+ });
1665
+ await this.searchService.sync(entityId);
1666
+ for (const fact of bundle.facts) {
1667
+ if (!fact.deleted_at && upsertedFactIds.has(fact.id) && !factsWithPreservedBlob.has(fact.id)) {
1668
+ const embedded = await this.embeddingService.embedFact({
1669
+ id: fact.id,
1670
+ entity_id: entityId,
1671
+ title: fact.title,
1672
+ body: fact.body,
1673
+ tags: Array.isArray(fact.tags) || typeof fact.tags === "string" ? fact.tags : []
1674
+ });
1675
+ if (!embedded) {
1676
+ await this.embeddingService.notifyEmbeddingPersisted(entityId, fact.id, null);
1677
+ }
1678
+ }
1679
+ }
1680
+ for (const fact of bundle.facts) {
1681
+ const blobData = factsWithPreservedBlob.get(fact.id);
1682
+ if (blobData && !fact.deleted_at && upsertedFactIds.has(fact.id)) {
1683
+ try {
1684
+ const float32Vector = new Float32Array(
1685
+ blobData.buffer,
1686
+ blobData.byteOffset,
1687
+ blobData.byteLength / 4
1688
+ );
1689
+ await this.embeddingService.notifyEmbeddingPersisted(
1690
+ entityId,
1691
+ fact.id,
1692
+ float32Vector
1693
+ );
1694
+ } catch (hookErr) {
1695
+ console.warn(
1696
+ `[WikiMemory] onEmbeddingPersisted hook failed for preserved-blob fact ${fact.id}:`,
1697
+ hookErr
1698
+ );
1699
+ }
1700
+ }
1701
+ }
1702
+ for (const factId of softDeletedFactIds) {
1703
+ if (!upsertedFactIds.has(factId) || upsertedDeletedFactIds.has(factId)) {
1704
+ try {
1705
+ await this.embeddingService.notifyEmbeddingPersisted(
1706
+ entityId,
1707
+ factId,
1708
+ null
1709
+ );
1710
+ } catch (hookErr) {
1711
+ console.warn(
1712
+ `[WikiMemory] onEmbeddingPersisted(vector=null) hook failed for soft-deleted fact ${factId}:`,
1713
+ hookErr
1714
+ );
1715
+ }
1716
+ }
1717
+ }
1718
+ try {
1719
+ const canonicalDimValue = await this.metadataRepo.getMeta(
1720
+ "embedding_dimension"
1721
+ );
1722
+ const canonicalDim = canonicalDimValue ? parseInt(canonicalDimValue, 10) : null;
1723
+ if (preservedBlobDims.size === 1) {
1724
+ const preservedDim = [...preservedBlobDims][0];
1725
+ if (canonicalDim === null || canonicalDim === preservedDim) {
1726
+ await this.embeddingService.storeEmbeddingDimension(preservedDim);
1727
+ const staleMismatchValue = await this.metadataRepo.getMeta(
1728
+ "embedding_dimension_mismatch"
1729
+ );
1730
+ if (staleMismatchValue && parseInt(staleMismatchValue, 10) !== preservedDim) {
1731
+ await this.metadataRepo.setMeta(
1732
+ "embedding_dimension_mismatch",
1733
+ String(preservedDim),
1734
+ this.db
1735
+ );
1736
+ }
1737
+ await this.embeddingService.reconcileEmbeddingDimension();
1738
+ } else {
1739
+ await this.metadataRepo.setMeta(
1740
+ "embedding_dimension_mismatch",
1741
+ String(canonicalDim),
1742
+ this.db
1743
+ );
1744
+ }
1745
+ } else if (preservedBlobDims.size > 1) {
1746
+ if (canonicalDim === null) {
1747
+ const sortedPreservedBlobDims = [...preservedBlobDims].sort(
1748
+ (a, b) => a - b
1749
+ );
1750
+ await this.embeddingService.storeEmbeddingDimension(
1751
+ sortedPreservedBlobDims[0]
1752
+ );
1753
+ await this.metadataRepo.setMeta(
1754
+ "embedding_dimension_mismatch",
1755
+ String(sortedPreservedBlobDims[0]),
1756
+ this.db
1757
+ );
1758
+ } else {
1759
+ await this.metadataRepo.setMeta(
1760
+ "embedding_dimension_mismatch",
1761
+ String(canonicalDim),
1762
+ this.db
1763
+ );
1764
+ }
1765
+ }
1766
+ } finally {
1767
+ this.searchService.evictCache(entityId);
1768
+ }
1769
+ }
1770
+ _warnCrossEntityCollision(type, id, existingEntityId, targetEntityId) {
1771
+ console.warn(
1772
+ `[WikiMemory] importDump: ${type} id "${id}" already belongs to entity "${existingEntityId}"; skipping for entity "${targetEntityId}"`
1773
+ );
1774
+ }
1775
+ _normalizeImportedSourceType(raw, ctx) {
1776
+ if (raw === "user_document") return "immutable_document";
1777
+ if (raw === "agent_inferred") return "librarian_inferred";
1778
+ const allowed = [
1779
+ "user_stated",
1780
+ "librarian_inferred",
1781
+ "user_confirmed",
1782
+ "immutable_document"
1783
+ ];
1784
+ if (allowed.includes(raw))
1785
+ return raw;
1786
+ const where = ctx !== void 0 ? ` for entity "${ctx.entityId}" fact "${ctx.factId}"` : "";
1787
+ throw new Error(
1788
+ `importDump: invalid source_type "${raw}"${where} (expected one of: ${allowed.join(", ")}, or legacy aliases user_document / agent_inferred)`
1789
+ );
1790
+ }
1791
+ async assertNoLegacySourceTypes() {
1792
+ if (!await this.entryRepo.hasLegacySourceTypes()) return;
1793
+ const count = await this.entryRepo.countLegacySourceTypes();
1794
+ throw new Error(
1795
+ `Database contains ${count} entries with legacy source_type values ('user_document' or 'agent_inferred'). These enum values were renamed in this release. Running without migration would allow legacy 'user_document' facts to bypass immutability guards, causing data corruption.
1796
+
1797
+ ${this.entryRepo.getLegacyMigrationSQL()}
1798
+
1799
+ After running the migration SQL, restart your application.`
1800
+ );
1801
+ }
1802
+ };
1803
+
1804
+ // src/services/EmbeddingService.ts
1805
+ var EmbeddingService = class {
1806
+ constructor(db, options, entryRepo, metadataRepo) {
1807
+ this.db = db;
1808
+ this.options = options;
1809
+ this.entryRepo = entryRepo;
1810
+ this.metadataRepo = metadataRepo;
1811
+ }
1812
+ async storeEmbeddingDimension(dim) {
1813
+ const existing = await this.metadataRepo.getMeta("embedding_dimension");
1814
+ if (existing) {
1815
+ const storedDim = parseInt(existing, 10);
1816
+ if (storedDim !== dim) {
1817
+ console.warn(
1818
+ `[WikiMemory] Embedding dimension mismatch: stored ${storedDim}, got ${dim}. Call runReembed() to rebuild embeddings with the new model.`
1819
+ );
1820
+ await this.metadataRepo.setMeta("embedding_dimension_mismatch", String(dim), this.db);
1821
+ }
1822
+ } else {
1823
+ await this.metadataRepo.setMeta("embedding_dimension", String(dim), this.db);
1824
+ }
1825
+ }
1826
+ /** Promotes embedding_dimension_mismatch to canonical embedding_dimension when safe. */
1827
+ async reconcileEmbeddingDimension() {
1828
+ const mismatchValue = await this.metadataRepo.getMeta("embedding_dimension_mismatch");
1829
+ if (!mismatchValue) return;
1830
+ const newDim = parseInt(mismatchValue, 10);
1831
+ const residualCount = await this.entryRepo.countStaleEmbeddings(newDim);
1832
+ if (residualCount === 0) {
1833
+ await this.metadataRepo.setMeta("embedding_dimension", mismatchValue, this.db);
1834
+ await this.metadataRepo.clearDimensionMismatch(this.db);
1835
+ }
1836
+ }
1837
+ async embedFact(fact) {
1838
+ const embedFn = this.options.llmProvider.embed;
1839
+ if (!embedFn) return false;
1840
+ let tagsStr;
1841
+ if (Array.isArray(fact.tags)) {
1842
+ tagsStr = fact.tags.join(" ");
1843
+ } else {
1844
+ try {
1845
+ const parsed = JSON.parse(fact.tags);
1846
+ tagsStr = Array.isArray(parsed) ? parsed.join(" ") : fact.tags;
1847
+ } catch {
1848
+ tagsStr = fact.tags;
1849
+ }
1850
+ }
1851
+ const text = `${fact.title} ${fact.body} ${tagsStr}`.trim();
1852
+ try {
1853
+ const vector = await embedFn(text);
1854
+ if (vector.length === 0 || !vector.every((v) => typeof v === "number" && isFinite(v))) {
1855
+ console.warn(`[WikiMemory] embedFact: embed() returned an invalid vector for ${fact.id}; skipping.`);
1856
+ return false;
1857
+ }
1858
+ const float32Vector = new Float32Array(vector);
1859
+ let hasNonFinite = false;
1860
+ for (let i = 0; i < float32Vector.length; i++) {
1861
+ if (!isFinite(float32Vector[i])) {
1862
+ hasNonFinite = true;
1863
+ break;
1864
+ }
1865
+ }
1866
+ if (hasNonFinite) {
1867
+ console.warn(`[WikiMemory] embedFact: embed() returned values that overflow float32 for ${fact.id}; skipping.`);
1868
+ return false;
1869
+ }
1870
+ await this.storeEmbeddingDimension(float32Vector.length);
1871
+ const blob = new Uint8Array(float32Vector.buffer);
1872
+ await this.entryRepo.updateEmbeddingBlob(fact.id, blob);
1873
+ try {
1874
+ await this.notifyEmbeddingPersisted(fact.entity_id, fact.id, float32Vector);
1875
+ } catch (hookErr) {
1876
+ console.warn(`[WikiMemory] onEmbeddingPersisted hook failed for ${fact.id}:`, hookErr);
1877
+ }
1878
+ return true;
1879
+ } catch (err) {
1880
+ console.warn(`[WikiMemory] embedFact failed for ${fact.id}:`, err);
1881
+ return false;
1882
+ }
1883
+ }
1884
+ async notifyEmbeddingPersisted(entityId, factId, vector) {
1885
+ if (!this.options.vectorRanker?.onEmbeddingPersisted) return;
1886
+ const vectorCopy = vector ? vector.slice() : null;
1887
+ await this.options.vectorRanker.onEmbeddingPersisted({
1888
+ entityId,
1889
+ factId,
1890
+ vector: vectorCopy
1891
+ });
1892
+ }
1893
+ async notifyEmbeddingPersistedOrThrow(entityId, factId, vector) {
1894
+ if (!this.options.vectorRanker?.onEmbeddingPersisted) return;
1895
+ if (this.options.forceDeleteIgnoreRankerHook === true) return;
1896
+ const vectorCopy = vector ? vector.slice() : null;
1897
+ const rawTimeout = this.options.deletionHookTimeoutMs ?? 3e4;
1898
+ if (typeof rawTimeout !== "number" || !Number.isFinite(rawTimeout) || rawTimeout <= 0) {
1899
+ throw new Error("Invalid deletionHookTimeoutMs: must be a positive finite number");
1900
+ }
1901
+ const timeoutMs = rawTimeout;
1902
+ let timeoutHandle;
1903
+ const timeoutPromise = new Promise((_, reject) => {
1904
+ timeoutHandle = setTimeout(() => {
1905
+ const timeoutError = new Error(`onEmbeddingPersisted timed out after ${timeoutMs}ms`);
1906
+ timeoutError[HOOK_TIMEOUT_MARKER] = true;
1907
+ reject(timeoutError);
1908
+ }, timeoutMs);
1909
+ });
1910
+ const hookPromise = Promise.resolve().then(
1911
+ () => this.options.vectorRanker.onEmbeddingPersisted({
1912
+ entityId,
1913
+ factId,
1914
+ vector: vectorCopy
1915
+ })
1916
+ );
1917
+ try {
1918
+ await Promise.race([hookPromise, timeoutPromise]);
1919
+ } catch (err) {
1920
+ hookPromise.catch(() => {
1921
+ });
1922
+ throw err;
1923
+ } finally {
1924
+ if (timeoutHandle) clearTimeout(timeoutHandle);
1925
+ }
1926
+ }
1927
+ };
1928
+
1929
+ // src/readOptions.ts
1930
+ function normalizeEntityIds(entityId) {
1931
+ const input = Array.isArray(entityId) ? entityId : [entityId];
1932
+ const seen = /* @__PURE__ */ new Set();
1933
+ const normalized = [];
1934
+ for (const id of input) {
1935
+ if (seen.has(id)) continue;
1936
+ seen.add(id);
1937
+ normalized.push(id);
1938
+ }
1939
+ return normalized;
1940
+ }
1941
+ function sanitizeTierWeights(entityIds, tierWeights) {
1942
+ if (tierWeights === void 0) return void 0;
1943
+ const sanitized = /* @__PURE__ */ Object.create(null);
1944
+ for (const entityId of entityIds) {
1945
+ const raw = tierWeights[entityId];
1946
+ if (raw === void 0 || !Number.isFinite(raw)) {
1947
+ sanitized[entityId] = 1;
1948
+ } else {
1949
+ sanitized[entityId] = Math.max(0, raw);
1950
+ }
1951
+ }
1952
+ return sanitized;
1953
+ }
1954
+ function applyTierWeight(score, entityId, sanitizedTierWeights) {
1955
+ const weight = sanitizedTierWeights?.[entityId] ?? 1;
1956
+ if (weight === 0) return -Infinity;
1957
+ return score * weight;
1958
+ }
1959
+ function shouldExposeReadMetadata(entityId) {
1960
+ return Array.isArray(entityId);
1961
+ }
1962
+
1963
+ // src/services/RetrievalService.ts
1964
+ var RetrievalService = class {
1965
+ constructor(options, entryRepo, taskRepo, eventRepo, metadataRepo, searchService) {
1966
+ this.options = options;
1967
+ this.entryRepo = entryRepo;
1968
+ this.taskRepo = taskRepo;
1969
+ this.eventRepo = eventRepo;
1970
+ this.metadataRepo = metadataRepo;
1971
+ this.searchService = searchService;
1972
+ }
1973
+ async read(entityId, query, options) {
1974
+ const config = this.options.config;
1975
+ const entityIds = normalizeEntityIds(entityId);
1976
+ const sanitizedTierWeights = shouldExposeReadMetadata(entityId) ? sanitizeTierWeights(entityIds, options?.tierWeights) : void 0;
1977
+ const exposeMetadata = shouldExposeReadMetadata(entityId);
1978
+ if (entityIds.length === 0) {
1979
+ const empty = { facts: [], tasks: [], events: [] };
1980
+ if (exposeMetadata) {
1981
+ empty.metadata = { query, entityIds: [] };
1982
+ if (sanitizedTierWeights && Object.keys(sanitizedTierWeights).length > 0) empty.metadata.tierWeights = sanitizedTierWeights;
1983
+ }
1984
+ return empty;
1985
+ }
1986
+ const MAX_ENTITY_IDS = 100;
1987
+ if (entityIds.length > MAX_ENTITY_IDS) {
1988
+ throw new RangeError(`read() accepts at most ${MAX_ENTITY_IDS} entity IDs; received ${entityIds.length}`);
1989
+ }
1990
+ const nullByteId = entityIds.find((id) => id.includes("\0"));
1991
+ if (nullByteId !== void 0) {
1992
+ throw new TypeError(`entity_id values must not contain the null byte (\\x00); got "${nullByteId}"`);
1993
+ }
1994
+ const rawMaxResults = options?.maxResults ?? config?.maxResults ?? config?.maxFtsResults ?? 10;
1995
+ const maxResults = Number.isFinite(rawMaxResults) ? Math.max(0, Math.trunc(rawMaxResults)) : 10;
1996
+ const rawPreFilterLimit = options?.preFilterLimit === null ? void 0 : options?.preFilterLimit ?? config?.preFilterLimit;
1997
+ const effectivePreFilterLimit = rawPreFilterLimit === void 0 ? void 0 : Number.isFinite(rawPreFilterLimit) ? Math.max(0, Math.trunc(rawPreFilterLimit)) : void 0;
1998
+ const hybridWeight = options?.hybridWeight ?? config?.hybridWeight;
1999
+ const weight = hybridWeight !== void 0 && !Number.isNaN(hybridWeight) ? Math.max(0, Math.min(1, hybridWeight)) : void 0;
2000
+ const skipEmbed = weight === 0;
2001
+ const embedFn = this.options.llmProvider.embed;
2002
+ const trimmedQuery = query.trim();
2003
+ let facts = [];
2004
+ let scoreByFactId;
2005
+ if (maxResults === 0) ; else if (trimmedQuery) {
2006
+ let usedEmbed = false;
2007
+ const scoredEntityIds = this._filterScoredEntities(entityIds, sanitizedTierWeights, options?.includeZeroWeightEntities);
2008
+ if (scoredEntityIds.length === 0) {
2009
+ usedEmbed = true;
2010
+ } else if (!skipEmbed && embedFn) {
2011
+ let rankerShouldRethrow = false;
2012
+ let pendingRankerFallbackError;
2013
+ try {
2014
+ const queryVec = await embedFn(trimmedQuery);
2015
+ if (queryVec.length === 0 || !queryVec.every((v) => typeof v === "number" && isFinite(v))) {
2016
+ throw new Error(
2017
+ "embed() returned an empty or non-finite vector. Falling back to keyword search."
2018
+ );
2019
+ }
2020
+ const storedDimValue = await this.metadataRepo.getMeta("embedding_dimension");
2021
+ if (storedDimValue) {
2022
+ const storedDim = parseInt(storedDimValue, 10);
2023
+ if (storedDim !== queryVec.length) {
2024
+ throw new Error(
2025
+ `Embedding dimension mismatch: stored ${storedDim}, query has ${queryVec.length}. Call runReembed() to rebuild embeddings with the new model.`
2026
+ );
2027
+ }
2028
+ }
2029
+ const mismatchedCount = await this.entryRepo.countDimensionMismatched(scoredEntityIds, queryVec.length);
2030
+ if (mismatchedCount > 0) {
2031
+ throw new Error(
2032
+ `Some facts have embeddings that do not match the current model dimension. Call runReembed() to rebuild all embeddings consistently.`
2033
+ );
2034
+ }
2035
+ const useRanker = Boolean(this.options.vectorRanker);
2036
+ let candidateRows;
2037
+ let populateCache = entityIds.length === 1;
2038
+ let miniSearchScores;
2039
+ if (effectivePreFilterLimit !== void 0) {
2040
+ populateCache = false;
2041
+ const preResults = this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, Number.MAX_SAFE_INTEGER);
2042
+ if (preResults.length === 0) {
2043
+ candidateRows = null;
2044
+ } else {
2045
+ const topKResults = preResults.slice(0, effectivePreFilterLimit);
2046
+ if (topKResults.length === 0) {
2047
+ candidateRows = null;
2048
+ } else {
2049
+ const topKIds = topKResults.map((r) => r.id);
2050
+ if (useRanker) {
2051
+ candidateRows = await this.entryRepo.findMetadataByIds(topKIds);
2052
+ } else {
2053
+ candidateRows = await this.entryRepo.findWithEmbeddingsByIds(topKIds);
2054
+ }
2055
+ if (weight !== void 0 && weight < 1) {
2056
+ const maxMsScore = Math.max(1, topKResults[0]?.score ?? 1);
2057
+ miniSearchScores = new Map(topKResults.map((r) => [r.id, r.score / maxMsScore]));
2058
+ }
2059
+ }
2060
+ }
2061
+ } else {
2062
+ if (useRanker) {
2063
+ candidateRows = await this.entryRepo.findMetadataByEntityIds(scoredEntityIds);
2064
+ } else {
2065
+ candidateRows = await this.entryRepo.findWithEmbeddingsByEntityIds(scoredEntityIds);
2066
+ }
2067
+ if (weight !== void 0 && weight < 1) {
2068
+ miniSearchScores = this.searchService.getMiniSearchScores(trimmedQuery, scoredEntityIds);
2069
+ }
2070
+ }
2071
+ if (candidateRows === null) {
2072
+ usedEmbed = true;
2073
+ } else {
2074
+ const entityCacheKey = entityIds.length === 1 ? entityIds[0] : entityIds.join("\0");
2075
+ let scored;
2076
+ if (useRanker) {
2077
+ const candidateRowsByEntity = /* @__PURE__ */ new Map();
2078
+ for (const row of candidateRows) {
2079
+ const rows = candidateRowsByEntity.get(row.entity_id) ?? [];
2080
+ rows.push(row);
2081
+ candidateRowsByEntity.set(row.entity_id, rows);
2082
+ }
2083
+ try {
2084
+ const rankerResultsByEntity = await Promise.all(
2085
+ scoredEntityIds.filter((id) => (candidateRowsByEntity.get(id)?.length ?? 0) > 0).map(async (scopedEntityId) => {
2086
+ const rowsForEntity = candidateRowsByEntity.get(scopedEntityId) ?? [];
2087
+ const candidateIds = effectivePreFilterLimit !== void 0 ? rowsForEntity.map((row) => row.id) : void 0;
2088
+ const ranked = await this._rankWithVectorRanker({
2089
+ entityId: scopedEntityId,
2090
+ queryVec,
2091
+ candidateIds,
2092
+ candidateRows: rowsForEntity,
2093
+ weight,
2094
+ miniSearchScores,
2095
+ limit: Math.max(maxResults * 2, maxResults + 50)
2096
+ });
2097
+ return ranked.map((row) => ({ ...row, entity_id: scopedEntityId }));
2098
+ })
2099
+ );
2100
+ scored = rankerResultsByEntity.flat();
2101
+ const scoredIds = new Set(scored.map((s) => s.id));
2102
+ const metadataById = new Map(
2103
+ candidateRows.filter((row) => scoredIds.has(row.id)).map((row) => [row.id, row])
2104
+ );
2105
+ scored = scored.map((row) => {
2106
+ const metadata = metadataById.get(row.id);
2107
+ return {
2108
+ ...row,
2109
+ updated_at: metadata?.updated_at ?? null,
2110
+ access_count: metadata?.access_count ?? null
2111
+ };
2112
+ });
2113
+ const isHybrid = weight !== void 0 && weight < 1;
2114
+ const maxBackfill = isHybrid ? maxResults : Math.max(0, maxResults - scored.length);
2115
+ if (maxBackfill > 0) {
2116
+ if (isHybrid) {
2117
+ const topK = [];
2118
+ for (const row of candidateRows) {
2119
+ if (scoredIds.has(row.id)) continue;
2120
+ const kwScore = miniSearchScores?.get(row.id) ?? 0;
2121
+ const candidate = { row, kwScore };
2122
+ if (topK.length < maxBackfill) {
2123
+ let insertIdx = topK.length;
2124
+ for (let i = 0; i < topK.length; i++) {
2125
+ const cmp = this._compareScoredRows(
2126
+ {
2127
+ id: candidate.row.id,
2128
+ score: candidate.kwScore,
2129
+ updated_at: candidate.row.updated_at,
2130
+ access_count: candidate.row.access_count
2131
+ },
2132
+ {
2133
+ id: topK[i].row.id,
2134
+ score: topK[i].kwScore,
2135
+ updated_at: topK[i].row.updated_at,
2136
+ access_count: topK[i].row.access_count
2137
+ }
2138
+ );
2139
+ if (cmp < 0) {
2140
+ insertIdx = i;
2141
+ break;
2142
+ }
2143
+ }
2144
+ topK.splice(insertIdx, 0, candidate);
2145
+ } else {
2146
+ const cmpWorst = this._compareScoredRows(
2147
+ {
2148
+ id: candidate.row.id,
2149
+ score: candidate.kwScore,
2150
+ updated_at: candidate.row.updated_at,
2151
+ access_count: candidate.row.access_count
2152
+ },
2153
+ {
2154
+ id: topK[maxBackfill - 1].row.id,
2155
+ score: topK[maxBackfill - 1].kwScore,
2156
+ updated_at: topK[maxBackfill - 1].row.updated_at,
2157
+ access_count: topK[maxBackfill - 1].row.access_count
2158
+ }
2159
+ );
2160
+ if (cmpWorst < 0) {
2161
+ let insertIdx = maxBackfill - 1;
2162
+ for (let i = 0; i < topK.length; i++) {
2163
+ const cmp = this._compareScoredRows(
2164
+ {
2165
+ id: candidate.row.id,
2166
+ score: candidate.kwScore,
2167
+ updated_at: candidate.row.updated_at,
2168
+ access_count: candidate.row.access_count
2169
+ },
2170
+ {
2171
+ id: topK[i].row.id,
2172
+ score: topK[i].kwScore,
2173
+ updated_at: topK[i].row.updated_at,
2174
+ access_count: topK[i].row.access_count
2175
+ }
2176
+ );
2177
+ if (cmp < 0) {
2178
+ insertIdx = i;
2179
+ break;
2180
+ }
2181
+ }
2182
+ topK.splice(insertIdx, 0, candidate);
2183
+ topK.pop();
2184
+ }
2185
+ }
2186
+ }
2187
+ for (const { row, kwScore } of topK) {
2188
+ scored.push({
2189
+ id: row.id,
2190
+ entity_id: row.entity_id,
2191
+ score: (1 - weight) * kwScore,
2192
+ updated_at: row.updated_at,
2193
+ access_count: row.access_count
2194
+ });
2195
+ }
2196
+ } else {
2197
+ const omitted = [];
2198
+ for (const row of candidateRows) {
2199
+ if (scoredIds.has(row.id)) continue;
2200
+ omitted.push({ id: row.id, entity_id: row.entity_id, score: -2, updated_at: row.updated_at, access_count: row.access_count });
2201
+ }
2202
+ if (omitted.length > 0) {
2203
+ this._tieBreakSort(omitted);
2204
+ scored.push(...omitted.slice(0, maxBackfill));
2205
+ }
2206
+ }
2207
+ }
2208
+ } catch (rankerErr) {
2209
+ const rankerError = rankerErr instanceof Error ? rankerErr : new Error(String(rankerErr));
2210
+ const policy = this.options.vectorRankerFallback ?? "js-cosine";
2211
+ this.options.onVectorRankerFallback?.({
2212
+ error: this._sanitizeRankerError(rankerError),
2213
+ policy
2214
+ });
2215
+ if (policy === "throw") {
2216
+ rankerShouldRethrow = true;
2217
+ throw rankerError;
2218
+ } else if (policy === "js-cosine") {
2219
+ let fallbackRows = candidateRows;
2220
+ if (fallbackRows && fallbackRows.length > 0 && !("embedding_blob" in fallbackRows[0])) {
2221
+ const rowIds = fallbackRows.map((r) => r.id);
2222
+ const embeddingRows = await this.entryRepo.findEmbeddingsByIds(rowIds);
2223
+ const embeddingsMap = new Map(embeddingRows.map((row) => [row.id, row]));
2224
+ fallbackRows = fallbackRows.map((r) => ({
2225
+ ...r,
2226
+ embedding_blob: embeddingsMap.get(r.id)?.embedding_blob ?? null,
2227
+ embedding: embeddingsMap.get(r.id)?.embedding ?? null
2228
+ }));
2229
+ }
2230
+ scored = await this.searchService.rankSemantic({
2231
+ entityId: entityCacheKey,
2232
+ queryVec,
2233
+ candidateRows: fallbackRows,
2234
+ weight,
2235
+ miniSearchScores,
2236
+ populateCache,
2237
+ limit: fallbackRows.length,
2238
+ skipSort: true
2239
+ // read() re-sorts after applying tier weights
2240
+ });
2241
+ } else if (policy === "keyword") {
2242
+ const keywordOversampledLimit = Math.max(maxResults * 2, maxResults + 50);
2243
+ const topResults = this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, keywordOversampledLimit);
2244
+ const topResultIds = new Set(topResults.map((r) => r.id));
2245
+ const candidateMap = new Map(candidateRows.filter((r) => topResultIds.has(r.id)).map((row) => [row.id, row]));
2246
+ scored = topResults.map((result) => {
2247
+ const metadata = candidateMap.get(result.id);
2248
+ const entityForScore = metadata?.entity_id ?? result.entity_id ?? "";
2249
+ return {
2250
+ id: result.id,
2251
+ entity_id: entityForScore,
2252
+ score: result.score ?? 0,
2253
+ access_count: metadata?.access_count ?? null,
2254
+ updated_at: metadata?.updated_at ?? null
2255
+ };
2256
+ });
2257
+ } else {
2258
+ scored = [];
2259
+ }
2260
+ if (this.options.propagateRankerFailureToRetrievalFallback) {
2261
+ const mirrored = new Error("Vector ranker failed, falling back", {
2262
+ cause: this._sanitizeRankerError(rankerErr)
2263
+ });
2264
+ pendingRankerFallbackError = mirrored;
2265
+ }
2266
+ }
2267
+ } else {
2268
+ const jsCosineNeedsTierSort = sanitizedTierWeights !== void 0 && Object.values(sanitizedTierWeights).some((w) => w !== 1);
2269
+ scored = await this.searchService.rankSemantic({
2270
+ entityId: entityCacheKey,
2271
+ queryVec,
2272
+ candidateRows,
2273
+ weight,
2274
+ miniSearchScores,
2275
+ populateCache,
2276
+ limit: jsCosineNeedsTierSort ? candidateRows.length : maxResults,
2277
+ skipSort: jsCosineNeedsTierSort
2278
+ // read() re-sorts after applying tier weights
2279
+ });
2280
+ }
2281
+ if (scored.length > 0) {
2282
+ scored = scored.map((row) => ({
2283
+ ...row,
2284
+ score: applyTierWeight(row.score, row.entity_id, sanitizedTierWeights)
2285
+ }));
2286
+ this._tieBreakSort(scored);
2287
+ const selectedScored = scored.slice(0, maxResults);
2288
+ const topIds = selectedScored.map((s) => s.id);
2289
+ if (exposeMetadata && trimmedQuery) {
2290
+ scoreByFactId = new Map(selectedScored.map((s) => [s.id, Number.isFinite(s.score) ? s.score : 0]));
2291
+ }
2292
+ if (topIds.length > 0) {
2293
+ const facts2 = await this._hydrateFactsByIds(topIds, entityIds);
2294
+ if (facts2.length < topIds.length) {
2295
+ const hydrationById = new Set(facts2.map((f) => f.id));
2296
+ const missingIds = topIds.filter((id) => !hydrationById.has(id));
2297
+ const missingCount = missingIds.length;
2298
+ const sample = missingIds.slice(0, 5);
2299
+ const sampleSuffix = sample.length > 0 ? ` Missing ID sample: ${sample.join(", ")}${missingIds.length > sample.length ? ", ..." : ""}.` : "";
2300
+ const error = new Error(
2301
+ `Phase 2 fact hydration returned ${missingCount} fewer row(s) than ranked IDs. Rows may have been concurrently soft-deleted or filtered by deleted_at during hydration, or vector ranker output may include IDs that do not exist in requested entities.` + sampleSuffix
2302
+ );
2303
+ this.options.onRetrievalFallback?.(error);
2304
+ }
2305
+ facts = facts2;
2306
+ }
2307
+ if (pendingRankerFallbackError) {
2308
+ this.options.onRetrievalFallback?.(pendingRankerFallbackError);
2309
+ pendingRankerFallbackError = void 0;
2310
+ }
2311
+ usedEmbed = true;
2312
+ } else {
2313
+ if (pendingRankerFallbackError) {
2314
+ this.options.onRetrievalFallback?.(pendingRankerFallbackError);
2315
+ pendingRankerFallbackError = void 0;
2316
+ }
2317
+ usedEmbed = true;
2318
+ }
2319
+ }
2320
+ } catch (err) {
2321
+ const error = err instanceof Error ? err : new Error(String(err));
2322
+ if (rankerShouldRethrow) {
2323
+ throw error;
2324
+ }
2325
+ if (pendingRankerFallbackError) {
2326
+ error.cause = pendingRankerFallbackError;
2327
+ pendingRankerFallbackError = void 0;
2328
+ }
2329
+ this.options.onRetrievalFallback?.(error);
2330
+ }
2331
+ }
2332
+ if (!usedEmbed && scoredEntityIds.length > 0) {
2333
+ const fallbackOversampledLimit = Math.max(maxResults * 2, maxResults + 50);
2334
+ const results = this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, fallbackOversampledLimit);
2335
+ const candidates = results.map((r) => ({
2336
+ id: r.id,
2337
+ entity_id: r.entity_id,
2338
+ score: applyTierWeight(r.score ?? 0, r.entity_id, sanitizedTierWeights),
2339
+ updated_at: null,
2340
+ access_count: null
2341
+ }));
2342
+ this._tieBreakSort(candidates);
2343
+ const topCandidates = candidates.slice(0, maxResults);
2344
+ const topIds = topCandidates.map((c) => c.id);
2345
+ if (topIds.length > 0) {
2346
+ facts = await this._hydrateFactsByIds(topIds, entityIds);
2347
+ if (exposeMetadata) {
2348
+ scoreByFactId = new Map(topCandidates.map((c) => [c.id, Number.isFinite(c.score) ? c.score : 0]));
2349
+ }
2350
+ }
2351
+ }
2352
+ if (facts.length > 0) {
2353
+ const ids = facts.map((f) => f.id);
2354
+ const now = Date.now();
2355
+ await this.entryRepo.trackAccess(ids, now);
2356
+ }
2357
+ } else {
2358
+ facts = await this.entryRepo.findRecentByEntityIds(entityIds, maxResults);
2359
+ }
2360
+ const eventsLimit = Math.min(10 * entityIds.length, 100);
2361
+ const [tasks, events] = await Promise.all([
2362
+ this.taskRepo.findAllPending(entityIds, entityIds.length === 1 ? void 0 : Math.min(20 * entityIds.length, 200)),
2363
+ entityIds.length === 1 ? this.eventRepo.getRecent(entityIds[0], eventsLimit) : this.eventRepo.getRecentForEntities(entityIds, eventsLimit)
2364
+ ]);
2365
+ let factScores;
2366
+ if (exposeMetadata && trimmedQuery && scoreByFactId) {
2367
+ factScores = Object.fromEntries(facts.map((fact) => [fact.id, scoreByFactId.get(fact.id) ?? 0]));
2368
+ }
2369
+ const bundle = { facts, tasks, events: events.reverse() };
2370
+ if (exposeMetadata) {
2371
+ bundle.metadata = { query, entityIds };
2372
+ if (sanitizedTierWeights && Object.keys(sanitizedTierWeights).length > 0) bundle.metadata.tierWeights = sanitizedTierWeights;
2373
+ if (factScores && Object.keys(factScores).length > 0) bundle.factScores = factScores;
2374
+ }
2375
+ return bundle;
2376
+ }
2377
+ /**
2378
+ * Returns entity IDs that will participate in scored retrieval.
2379
+ * Excludes zero-weight entities unless includeZeroWeightEntities is true.
2380
+ */
2381
+ _filterScoredEntities(entityIds, sanitizedTierWeights, includeZeroWeightEntities) {
2382
+ return entityIds.filter((id) => {
2383
+ const w = sanitizedTierWeights?.[id] ?? 1;
2384
+ return includeZeroWeightEntities === true || w !== 0;
2385
+ });
2386
+ }
2387
+ /**
2388
+ * Stable tie-break sort: score desc → access_count desc → updated_at desc → id asc.
2389
+ */
2390
+ _tieBreakSort(items) {
2391
+ items.sort((a, b) => this._compareScoredRows(a, b));
2392
+ }
2393
+ /**
2394
+ * Comparator for score + deterministic tie-break fields.
2395
+ * Negative return means "a ranks ahead of b" for descending score order.
2396
+ */
2397
+ _compareScoredRows(a, b) {
2398
+ const scoreDiff = b.score - a.score;
2399
+ if (!Number.isNaN(scoreDiff) && scoreDiff !== 0) return scoreDiff;
2400
+ const accessCountDiff = (b.access_count ?? 0) - (a.access_count ?? 0);
2401
+ if (accessCountDiff !== 0) return accessCountDiff;
2402
+ const updatedAtDiff = (b.updated_at ?? 0) - (a.updated_at ?? 0);
2403
+ if (updatedAtDiff !== 0) return updatedAtDiff;
2404
+ return a.id.localeCompare(b.id);
2405
+ }
2406
+ /**
2407
+ * Hydrate full facts by ID. Pass scopedEntityIds to restrict to requested namespaces in SQL
2408
+ * (defense-in-depth against a rogue VectorRanker returning cross-entity IDs).
2409
+ */
2410
+ async _hydrateFactsByIds(ids, scopedEntityIds, tx) {
2411
+ return this.entryRepo.findByIds(ids, scopedEntityIds, tx);
2412
+ }
2413
+ _sanitizeRankerError(err) {
2414
+ return sanitizeRankerError(err, this.options.sanitizeRankerErrors);
2415
+ }
2416
+ /**
2417
+ * Delegate semantic ranking to the injected VectorRanker.
2418
+ * Caller should pass an oversampledLimit to preserve recall after re-ranking.
2419
+ * Returns scored results ready for hybrid blending and tie-break sorting.
2420
+ */
2421
+ async _rankWithVectorRanker(args) {
2422
+ const { entityId, candidateIds, candidateRows, weight, miniSearchScores, limit } = args;
2423
+ const ranker = this.options.vectorRanker;
2424
+ if (!ranker) {
2425
+ throw new Error("vectorRanker not configured");
2426
+ }
2427
+ const queryVecCopy = args.queryVec instanceof Float32Array ? args.queryVec.slice() : Array.from(args.queryVec);
2428
+ const rankerResults = await ranker.rankBySimilarity({
2429
+ entityId,
2430
+ queryVec: queryVecCopy,
2431
+ candidateIds,
2432
+ limit
2433
+ });
2434
+ const allowedIds = new Set(candidateRows.map((row) => row.id));
2435
+ const seen = /* @__PURE__ */ new Set();
2436
+ const normalized = [];
2437
+ for (const r of rankerResults) {
2438
+ if (normalized.length >= limit) break;
2439
+ if (seen.has(r.id)) continue;
2440
+ if (allowedIds && !allowedIds.has(r.id)) continue;
2441
+ if (!Number.isFinite(r.semanticScore)) continue;
2442
+ seen.add(r.id);
2443
+ normalized.push(r);
2444
+ }
2445
+ const entityIdByCandidateId = new Map(candidateRows.map((row) => [row.id, row.entity_id]));
2446
+ const scored = normalized.map((r) => {
2447
+ let score = r.semanticScore;
2448
+ if (weight !== void 0) {
2449
+ const kwScore = miniSearchScores?.get(r.id) ?? 0;
2450
+ score = weight * Math.max(0, r.semanticScore) + (1 - weight) * kwScore;
2451
+ }
2452
+ return {
2453
+ id: r.id,
2454
+ entity_id: entityIdByCandidateId.get(r.id),
2455
+ // allowedIds filter above guarantees membership
2456
+ score
2457
+ };
2458
+ });
2459
+ return scored;
2460
+ }
2461
+ };
2462
+
2463
+ // src/services/WriteService.ts
2464
+ var WriteService = class {
2465
+ constructor(db, options, eventRepo, metadataRepo, jobManager, maintenanceService) {
2466
+ this.db = db;
2467
+ this.options = options;
2468
+ this.eventRepo = eventRepo;
2469
+ this.metadataRepo = metadataRepo;
2470
+ this.jobManager = jobManager;
2471
+ this.maintenanceService = maintenanceService;
2472
+ }
2473
+ async write(entityId, event) {
2474
+ const id = generateId("evt_");
2475
+ const now = Date.now();
2476
+ let eventType = event.event_type;
2477
+ if (!["observation", "decision", "action", "outcome"].includes(eventType)) {
2478
+ eventType = "observation";
2479
+ }
2480
+ const newEvent = {
2481
+ id,
2482
+ entity_id: entityId,
2483
+ event_type: eventType,
2484
+ summary: event.summary,
2485
+ related_entry_id: event.related_entry_id || null,
2486
+ created_at: now
2487
+ };
2488
+ let shouldRunLibrarian = false;
2489
+ let librarianCount = 0;
2490
+ let prevMemoryCheckpoint = 0;
2491
+ await this.db.withTransactionAsync(async (tx) => {
2492
+ await this.eventRepo.add(newEvent, tx);
2493
+ const threshold = this.options.config?.autoLibrarianThreshold || 20;
2494
+ const [count, cp] = await Promise.all([
2495
+ this.eventRepo.count(entityId, tx),
2496
+ this.metadataRepo.getCheckpoint(entityId, tx)
2497
+ ]);
2498
+ let memoryCheckpoint = cp.memory ?? 0;
2499
+ if (memoryCheckpoint > count) memoryCheckpoint = 0;
2500
+ if (count - memoryCheckpoint >= threshold) {
2501
+ if (!this.jobManager.isBlocked("librarian", entityId)) {
2502
+ shouldRunLibrarian = true;
2503
+ librarianCount = count;
2504
+ prevMemoryCheckpoint = memoryCheckpoint;
2505
+ await this.metadataRepo.updateCheckpoint(entityId, { memory: count }, tx);
2506
+ }
2507
+ }
2508
+ });
2509
+ if (shouldRunLibrarian) {
2510
+ try {
2511
+ this.jobManager.acquireLock("librarian", entityId);
2512
+ this.runLibrarianThenMaybeHeal(entityId, librarianCount, prevMemoryCheckpoint).catch(console.error).finally(() => {
2513
+ this.jobManager.releaseLock("librarian", entityId);
2514
+ });
2515
+ } catch (e) {
2516
+ if (!(e instanceof WikiBusyError)) throw e;
2517
+ await this.metadataRepo.updateCheckpoint(entityId, { memory: prevMemoryCheckpoint }, this.db);
2518
+ }
2519
+ }
2520
+ }
2521
+ async runLibrarianThenMaybeHeal(entityId, currentEventCount, prevCheckpoint) {
2522
+ try {
2523
+ await this.maintenanceService.doRunLibrarian(entityId);
2524
+ await this.metadataRepo.updateCheckpoint(entityId, { memory: currentEventCount }, this.db);
2525
+ } catch (e) {
2526
+ await this.metadataRepo.updateCheckpoint(entityId, { memory: prevCheckpoint }, this.db);
2527
+ throw e;
2528
+ }
2529
+ const autoHealThreshold = this.options.config?.autoHealThreshold || 100;
2530
+ const cp = await this.metadataRepo.getCheckpoint(entityId, this.db);
2531
+ let healCheckpoint = cp.heal ?? 0;
2532
+ if (healCheckpoint > currentEventCount) healCheckpoint = 0;
2533
+ const shouldRunHeal = currentEventCount - healCheckpoint >= autoHealThreshold;
2534
+ if (shouldRunHeal && this.jobManager.tryAcquireAutoHealLock(entityId)) {
2535
+ try {
2536
+ await this.maintenanceService.doRunHeal(entityId);
2537
+ await this.metadataRepo.updateCheckpoint(entityId, { heal: currentEventCount }, this.db);
2538
+ } finally {
2539
+ this.jobManager.releaseLock("heal", entityId);
2540
+ }
2541
+ }
2542
+ }
2543
+ };
2544
+
2545
+ export { EmbeddingService, HOOK_TIMEOUT_MARKER, ImportExportService, IngestionService, JobManager, MaintenanceService, PromptService, PrunePartialFailureError, RetrievalService, SearchService, WikiBusyError, WriteService, __privateAdd, __privateGet, __privateSet, generateId, normalizeSourceHash, normalizeSourceRef, parseEmbedding };
2546
+ //# sourceMappingURL=chunk-2FGDZKC2.mjs.map
2547
+ //# sourceMappingURL=chunk-2FGDZKC2.mjs.map