@equationalapplications/core-llm-wiki 5.1.1 → 5.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/{chunk-YKXCMOHH.mjs → chunk-6HNIOKT4.mjs} +258 -70
- package/dist/chunk-6HNIOKT4.mjs.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +608 -85
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +354 -20
- package/dist/index.mjs.map +1 -1
- package/dist/{testing-DCRNie7k.d.mts → testing-CAk9oFvw.d.mts} +242 -3
- package/dist/{testing-DCRNie7k.d.ts → testing-CAk9oFvw.d.ts} +242 -3
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +309 -67
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +1 -1
- package/package.json +2 -2
- package/dist/chunk-YKXCMOHH.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -825,6 +825,20 @@ const { chunks, truncated } = chunkText(
|
|
|
825
825
|
|
|
826
826
|
> **Note:** `ingestDocument()` clamps the resolved overlap — whether it came from `chunkOverlap`, `WikiConfig`, or `DEFAULT_CHUNK_OVERLAP` — to `maxChunkLength - 1`. The clamp is evaluated on every call and is a no-op at the shipped defaults (`400 < 12000 - 1`), but it also bites when a custom `maxChunkLength` alone leaves the resolved overlap too large (e.g. `maxChunkLength: 100` with the default overlap `400` ingests at an effective overlap of `99`). That clamp is internal: passing the unclamped pair straight to `chunkText` throws, since it requires `overlap < maxChunkLength`. Apply the same `Math.min(overlap, maxChunkLength - 1)` yourself when re-chunking under a custom config.
|
|
827
827
|
|
|
828
|
+
## Schema Migrations
|
|
829
|
+
|
|
830
|
+
`wikiMemory.setup()` runs pending schema migrations in order and records the applied version. Migrations are additive and safe to re-run.
|
|
831
|
+
|
|
832
|
+
**Migration v9** (`add_live_hash_unique_index`) adds a partial `UNIQUE` index on `(entity_id, source_hash)` scoped to live (`deleted_at IS NULL`), non-null-hash rows. This closes a TOCTOU race where two concurrent `ingestDocument()` calls for different `sourceRef`s could both pass the duplicate-hash pre-check before either write committed, leaving two live rows with the same content hash.
|
|
833
|
+
|
|
834
|
+
If your existing database already has live rows that violate this invariant (created before the app-level race fix shipped), `setup()` throws:
|
|
835
|
+
|
|
836
|
+
```text
|
|
837
|
+
Migration v9 (add_live_hash_unique_index) failed: existing live rows violate the new UNIQUE index. …
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
The error lists the offending `(entity_id, source_hash)` groups. `setup()` performs no destructive cleanup on this path — the index is not created and the schema version is not advanced, so it's safe to retry after remediation. To remediate, either soft-delete (or re-ingest with a corrected `sourceRef`) all but one live row per listed group, then re-run `setup()`.
|
|
841
|
+
|
|
828
842
|
## Adapter Interface
|
|
829
843
|
|
|
830
844
|
Implement `SQLiteAdapter` to use your platform's SQLite driver:
|
|
@@ -38,6 +38,12 @@ var SQLITE_RESULT_CODE_NAMES = {
|
|
|
38
38
|
26: "SQLITE_NOTADB",
|
|
39
39
|
27: "SQLITE_NOTICE",
|
|
40
40
|
28: "SQLITE_WARNING",
|
|
41
|
+
// Extended result codes (https://sqlite.org/rescode.html#extended_result_code_list).
|
|
42
|
+
// 2067 = SQLITE_CONSTRAINT_UNIQUE — raised when a UNIQUE index/PRIMARY KEY
|
|
43
|
+
// rejects an INSERT/UPDATE; expo-sqlite reports this as a numeric "Error code N:"
|
|
44
|
+
// prefix and we normalize it back to the symbolic name so callers can branch on
|
|
45
|
+
// the same string the rest of the ecosystem uses.
|
|
46
|
+
2067: "SQLITE_CONSTRAINT_UNIQUE",
|
|
41
47
|
100: "SQLITE_ROW",
|
|
42
48
|
101: "SQLITE_DONE"
|
|
43
49
|
};
|
|
@@ -64,6 +70,15 @@ var WikiBusyError = class extends Error {
|
|
|
64
70
|
this.entityId = entityId;
|
|
65
71
|
}
|
|
66
72
|
};
|
|
73
|
+
var WikiDuplicateHashError = class extends Error {
|
|
74
|
+
constructor(params) {
|
|
75
|
+
super(`Duplicate source hash for entity ${params.entityId}; another ref already holds this content`);
|
|
76
|
+
this.name = "WikiDuplicateHashError";
|
|
77
|
+
this.canonical = params.canonical;
|
|
78
|
+
this.sourceHash = params.sourceHash;
|
|
79
|
+
this.entityId = params.entityId;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
67
82
|
var WikiTransactionError = class extends Error {
|
|
68
83
|
constructor(message, options) {
|
|
69
84
|
super(message, options);
|
|
@@ -491,6 +506,19 @@ var JobManager = class {
|
|
|
491
506
|
this.prefix = prefix;
|
|
492
507
|
this.activeMaintenanceJobs = /* @__PURE__ */ new Set();
|
|
493
508
|
this.activeIngestJobs = /* @__PURE__ */ new Map();
|
|
509
|
+
/**
|
|
510
|
+
* Per-(entityId, sourceHash) promise-chain lock. Serializes ingest races
|
|
511
|
+
* at the application level (the v9 partial UNIQUE index enforces it at
|
|
512
|
+
* the DB level as defense-in-depth). Two callers holding the same hash
|
|
513
|
+
* see FIFO order; different hashes never block one another.
|
|
514
|
+
*
|
|
515
|
+
* Keyed `${entityId}\0${sourceHash}` so distinct (entityId, sourceHash)
|
|
516
|
+
* pairs can never share a lock. A raw concatenation would let
|
|
517
|
+
* ("ab", "c…") collide with ("a", "bc…"); the NUL separator makes that
|
|
518
|
+
* impossible because neither component can contain NUL (entityIds are
|
|
519
|
+
* user-supplied identifiers; sourceHashes are hex digests).
|
|
520
|
+
*/
|
|
521
|
+
this.hashLocks = /* @__PURE__ */ new Map();
|
|
494
522
|
this.statusSubscribers = /* @__PURE__ */ new Map();
|
|
495
523
|
/**
|
|
496
524
|
* Lookup table for acquireLock/releaseLock's dynamic-dispatch branch.
|
|
@@ -508,6 +536,51 @@ var JobManager = class {
|
|
|
508
536
|
ontologyBackfill: (id) => this._ontologyBackfillKey(id)
|
|
509
537
|
};
|
|
510
538
|
}
|
|
539
|
+
_hashLockKey(entityId, sourceHash) {
|
|
540
|
+
return `${entityId}\0${sourceHash}`;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Acquire a promise-chain lock scoped to one `(entityId, sourceHash)` pair.
|
|
544
|
+
* Returns a zero-argument release closure that removes the current tail
|
|
545
|
+
* from the chain. Releasing is idempotent so a holder that double-fires
|
|
546
|
+
* (e.g. on a finally that races an explicit release) cannot unblock
|
|
547
|
+
* the next holder prematurely.
|
|
548
|
+
*
|
|
549
|
+
* Documented ordering: ingest acquires the hash lock FIRST, then the
|
|
550
|
+
* synchronous sourceRef ingest lock, and the release closure unwinds in
|
|
551
|
+
* the opposite order (sourceRef first, hash second). Hash-then-sourceRef
|
|
552
|
+
* is required because the v9 partial UNIQUE index conflicts on the
|
|
553
|
+
* (entity_id, source_hash) pair regardless of source_ref, so two callers
|
|
554
|
+
* with DIFFERENT source_refs racing the same hash are the exact case the
|
|
555
|
+
* TOCTOU race fix needs to serialize.
|
|
556
|
+
*
|
|
557
|
+
* Mechanics: each holder owns a private `released` promise that resolves
|
|
558
|
+
* only when THIS holder calls release(). The map stores the holder's
|
|
559
|
+
* `released` promise as the "tail" — the NEXT caller awaits that tail
|
|
560
|
+
* via `previous = this.hashLocks.get(key)`. The current caller gets
|
|
561
|
+
* their unique release function once `previous` (the prior holder's
|
|
562
|
+
* release signal) settles, so each holder is guaranteed a UNIQUE release
|
|
563
|
+
* closure bound to their own `released` promise.
|
|
564
|
+
*/
|
|
565
|
+
acquireHashLock(entityId, sourceHash) {
|
|
566
|
+
const key = this._hashLockKey(entityId, sourceHash);
|
|
567
|
+
const previous = this.hashLocks.get(key) ?? Promise.resolve();
|
|
568
|
+
let resolveReleased;
|
|
569
|
+
const released = new Promise((resolve) => {
|
|
570
|
+
resolveReleased = resolve;
|
|
571
|
+
});
|
|
572
|
+
let fired = false;
|
|
573
|
+
const release = () => {
|
|
574
|
+
if (fired) return;
|
|
575
|
+
fired = true;
|
|
576
|
+
if (this.hashLocks.get(key) === released) {
|
|
577
|
+
this.hashLocks.delete(key);
|
|
578
|
+
}
|
|
579
|
+
resolveReleased();
|
|
580
|
+
};
|
|
581
|
+
this.hashLocks.set(key, released);
|
|
582
|
+
return previous.then(() => release);
|
|
583
|
+
}
|
|
511
584
|
_pruneKey(entityId) {
|
|
512
585
|
return `${this.prefix}:${entityId}:prune`;
|
|
513
586
|
}
|
|
@@ -693,6 +766,19 @@ var JobManager = class {
|
|
|
693
766
|
return this.activeMaintenanceJobs.has(this.lockKeyFns[operation](entityId)) || this.activeMaintenanceJobs.has(this._pruneKey(entityId)) || this._isReembedActive(entityId) || this._isImportActiveFor(entityId) || this._isForgetActiveFor(entityId);
|
|
694
767
|
case "prune":
|
|
695
768
|
return this.activeMaintenanceJobs.has(this._pruneKey(entityId)) || this.activeMaintenanceJobs.has(this._librarianKey(entityId)) || this.activeMaintenanceJobs.has(this._healKey(entityId)) || this.activeMaintenanceJobs.has(this._ontologyBackfillKey(entityId)) || this._isReembedActive(entityId) || this._isIngestActiveFor(entityId) || this._isImportActiveFor(entityId) || this._isForgetActiveFor(entityId);
|
|
769
|
+
case "reembed":
|
|
770
|
+
return this.activeMaintenanceJobs.has(this._reembedKey(entityId)) || this.activeMaintenanceJobs.has(this._globalReembedKey()) || this.activeMaintenanceJobs.has(this._pruneKey(entityId)) || this.activeMaintenanceJobs.has(this._librarianKey(entityId)) || this.activeMaintenanceJobs.has(this._healKey(entityId)) || this.activeMaintenanceJobs.has(this._ontologyBackfillKey(entityId)) || this._isIngestActiveFor(entityId) || this._isImportActiveFor(entityId) || this._isForgetActiveFor(entityId);
|
|
771
|
+
case "global_reembed":
|
|
772
|
+
return this.activeMaintenanceJobs.has(this._globalReembedKey()) || this._isAnyMaintenanceActiveWithSuffix(":reembed") || this._isAnyMaintenanceActiveWithSuffix(":prune") || this._isAnyMaintenanceActiveWithSuffix(":librarian") || this._isAnyMaintenanceActiveWithSuffix(":heal") || this._isAnyMaintenanceActiveWithSuffix(":ontologyBackfill") || this.activeIngestJobs.size > 0 || this._isAnyMaintenanceActiveWithSuffix(":import") || this._isAnyMaintenanceActiveWithSuffix(":forget");
|
|
773
|
+
case "import":
|
|
774
|
+
case "forget": {
|
|
775
|
+
const selfKey = operation === "import" ? this._importKey(entityId) : this._forgetKey(entityId);
|
|
776
|
+
return this.activeMaintenanceJobs.has(selfKey) || this.activeMaintenanceJobs.has(this._librarianKey(entityId)) || this.activeMaintenanceJobs.has(this._healKey(entityId)) || this.activeMaintenanceJobs.has(this._ontologyBackfillKey(entityId)) || this.activeMaintenanceJobs.has(this._pruneKey(entityId)) || this._isReembedActive(entityId) || this._isIngestActiveFor(entityId) || this._isImportActiveFor(entityId) || this._isForgetActiveFor(entityId);
|
|
777
|
+
}
|
|
778
|
+
case "global_import":
|
|
779
|
+
return this.activeMaintenanceJobs.has(this._globalImportKey());
|
|
780
|
+
case "ingest":
|
|
781
|
+
return this._hasIngestJob(entityId) || this.activeMaintenanceJobs.has(this._pruneKey(entityId)) || this._isReembedActive(entityId) || this._isImportActiveFor(entityId) || this._isForgetActiveFor(entityId);
|
|
696
782
|
default:
|
|
697
783
|
return false;
|
|
698
784
|
}
|
|
@@ -708,6 +794,38 @@ var JobManager = class {
|
|
|
708
794
|
this._notifyStatusSubscribers(entityId);
|
|
709
795
|
return true;
|
|
710
796
|
}
|
|
797
|
+
/**
|
|
798
|
+
* Centralized ingest lock acquisition. Acquires the hash lock FIRST
|
|
799
|
+
* (FIFO across callers racing the same hash), then the synchronous
|
|
800
|
+
* sourceRef ingest lock (rejects cross-entity conflicts and busy
|
|
801
|
+
* operations). Returns a single zero-argument release closure that
|
|
802
|
+
* unwinds in the opposite order — sourceRef first, hash second — so
|
|
803
|
+
* the sourceRef lock is freed the instant its work is done and only
|
|
804
|
+
* the hash lock keeps serializing the duplicate-content race window.
|
|
805
|
+
*
|
|
806
|
+
* On a sourceRef conflict (WikiBusyError thrown synchronously by the
|
|
807
|
+
* underlying acquireLock) the hash lock is released BEFORE the error
|
|
808
|
+
* propagates, so the next caller in the hash chain is not held up by
|
|
809
|
+
* a request that never reached the DB.
|
|
810
|
+
*/
|
|
811
|
+
async acquireIngestLocks(entityId, sourceRef, sourceHash) {
|
|
812
|
+
const releaseHash = await this.acquireHashLock(entityId, sourceHash);
|
|
813
|
+
let sourceLockAcquired = false;
|
|
814
|
+
try {
|
|
815
|
+
this.acquireLock("ingest", entityId, sourceRef);
|
|
816
|
+
sourceLockAcquired = true;
|
|
817
|
+
} catch (err) {
|
|
818
|
+
releaseHash();
|
|
819
|
+
throw err;
|
|
820
|
+
}
|
|
821
|
+
return () => {
|
|
822
|
+
try {
|
|
823
|
+
if (sourceLockAcquired) this.releaseLock("ingest", entityId, sourceRef);
|
|
824
|
+
} finally {
|
|
825
|
+
releaseHash();
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
}
|
|
711
829
|
/**
|
|
712
830
|
* Validates then acquires global + per-entity import locks atomically.
|
|
713
831
|
* Validates all entities before acquiring any lock (same as current importDump semantics).
|
|
@@ -1148,18 +1266,19 @@ var DEFAULT_CHUNK_OVERLAP = 400;
|
|
|
1148
1266
|
|
|
1149
1267
|
// src/services/IngestionService.ts
|
|
1150
1268
|
var IngestionService = class {
|
|
1151
|
-
constructor(db, prefix, options, entryRepo, searchService, jobManager, embeddingService, promptService, ontologyService) {
|
|
1269
|
+
constructor(db, prefix, options, entryRepo, sourceRefIndexRepo, searchService, jobManager, embeddingService, promptService, ontologyService) {
|
|
1152
1270
|
this.db = db;
|
|
1153
1271
|
this.prefix = prefix;
|
|
1154
1272
|
this.options = options;
|
|
1155
1273
|
this.entryRepo = entryRepo;
|
|
1274
|
+
this.sourceRefIndexRepo = sourceRefIndexRepo;
|
|
1156
1275
|
this.searchService = searchService;
|
|
1157
1276
|
this.jobManager = jobManager;
|
|
1158
1277
|
this.embeddingService = embeddingService;
|
|
1159
1278
|
this.ontologyService = ontologyService;
|
|
1160
1279
|
this.promptService = promptService ?? new PromptService(this.options.config?.prompts);
|
|
1161
1280
|
}
|
|
1162
|
-
async ingestDocument(entityId, params) {
|
|
1281
|
+
async ingestDocument(entityId, params, opts) {
|
|
1163
1282
|
const sourceRef = normalizeSourceRef(params.sourceRef);
|
|
1164
1283
|
if (!sourceRef) throw new Error("Invalid sourceRef");
|
|
1165
1284
|
const sourceHash = normalizeSourceHash(params.sourceHash);
|
|
@@ -1175,8 +1294,18 @@ var IngestionService = class {
|
|
|
1175
1294
|
if (typeof params.documentChunk !== "string") {
|
|
1176
1295
|
throw new Error(`documentChunk must be a string, received ${typeof params.documentChunk}`);
|
|
1177
1296
|
}
|
|
1178
|
-
this.jobManager.
|
|
1297
|
+
const releaseIngestLocks = await this.jobManager.acquireIngestLocks(entityId, sourceRef, sourceHash);
|
|
1179
1298
|
try {
|
|
1299
|
+
const onDuplicateHash = opts?.onDuplicateHash ?? "ingest";
|
|
1300
|
+
if (onDuplicateHash !== "ingest") {
|
|
1301
|
+
const canonical = await this.sourceRefIndexRepo.findActiveByEntityAndHash(entityId, sourceHash);
|
|
1302
|
+
if (canonical !== null && canonical !== sourceRef) {
|
|
1303
|
+
if (onDuplicateHash === "throw") {
|
|
1304
|
+
throw new WikiDuplicateHashError({ canonical, sourceHash, entityId });
|
|
1305
|
+
}
|
|
1306
|
+
return { truncated: false, chunks: 0, duplicateOf: canonical };
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1180
1309
|
const { chunks, truncated } = chunkText(params.documentChunk, maxChunkLength, chunkOverlap);
|
|
1181
1310
|
if (chunks.length === 0) return { truncated: false, chunks: 0 };
|
|
1182
1311
|
const chunkResults = await withConcurrency(
|
|
@@ -1212,68 +1341,81 @@ var IngestionService = class {
|
|
|
1212
1341
|
const now = Date.now();
|
|
1213
1342
|
const insertedFacts = [];
|
|
1214
1343
|
const deletedSourceFactIds = [];
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
for (const { facts, ontology_updates } of orderedChunkFacts) {
|
|
1230
|
-
if (mode === "emergent" && ontology_updates && this.ontologyService) {
|
|
1231
|
-
manifest = await this.ontologyService.mergeEmergentUpdates(entityId, ontology_updates, tx);
|
|
1232
|
-
ontologyState = await this.ontologyService.getEffectiveState(entityId, tx);
|
|
1233
|
-
mode = ontologyState.mode;
|
|
1344
|
+
try {
|
|
1345
|
+
await this.db.withTransactionAsync(async (tx) => {
|
|
1346
|
+
deletedSourceFactIds.push(...await this.entryRepo.findIdsBySource(entityId, sourceRef, null, tx, false));
|
|
1347
|
+
await this.entryRepo.softDeleteBySource(entityId, tx, sourceRef, null);
|
|
1348
|
+
await this.sourceRefIndexRepo.softDeleteByEntityAndSourceRef(entityId, sourceRef, tx);
|
|
1349
|
+
await this.sourceRefIndexRepo.upsert(entityId, sourceHash, sourceRef, tx);
|
|
1350
|
+
const titleIndex = /* @__PURE__ */ new Map();
|
|
1351
|
+
const pendingEdges = [];
|
|
1352
|
+
const existingFacts = await this.entryRepo.findRecentByEntityId(entityId, 500, tx);
|
|
1353
|
+
for (const existing of existingFacts) {
|
|
1354
|
+
titleIndex.set(normalizeTitleKey(existing.title), {
|
|
1355
|
+
id: existing.id,
|
|
1356
|
+
okf_type: existing.okf_type ?? null
|
|
1357
|
+
});
|
|
1234
1358
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1359
|
+
let ontologyState = await this.ontologyService?.getEffectiveState(entityId, tx) ?? { mode: "off", manifest: { node_types: [], edge_types: [] } };
|
|
1360
|
+
let { mode, manifest } = ontologyState;
|
|
1361
|
+
for (const { facts, ontology_updates } of orderedChunkFacts) {
|
|
1362
|
+
if (mode === "emergent" && ontology_updates && this.ontologyService) {
|
|
1363
|
+
manifest = await this.ontologyService.mergeEmergentUpdates(entityId, ontology_updates, tx);
|
|
1364
|
+
ontologyState = await this.ontologyService.getEffectiveState(entityId, tx);
|
|
1365
|
+
mode = ontologyState.mode;
|
|
1366
|
+
}
|
|
1367
|
+
for (const fact of facts) {
|
|
1368
|
+
const ontologyFact = fact;
|
|
1369
|
+
const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest) ?? { okf_type: null, edges: [] };
|
|
1370
|
+
const id = generateId("fact_");
|
|
1371
|
+
const wikiFact = {
|
|
1372
|
+
id,
|
|
1373
|
+
entity_id: entityId,
|
|
1374
|
+
title: fact.title,
|
|
1375
|
+
body: fact.body,
|
|
1376
|
+
tags: fact.tags,
|
|
1377
|
+
confidence: fact.confidence,
|
|
1378
|
+
source_type: "immutable_document",
|
|
1379
|
+
source_hash: sourceHash,
|
|
1380
|
+
source_ref: sourceRef,
|
|
1381
|
+
created_at: now,
|
|
1382
|
+
updated_at: now,
|
|
1383
|
+
last_accessed_at: null,
|
|
1384
|
+
access_count: 0,
|
|
1385
|
+
deleted_at: null,
|
|
1386
|
+
okf_type: normalized.okf_type
|
|
1387
|
+
};
|
|
1388
|
+
await this.entryRepo.upsert(wikiFact, tx);
|
|
1389
|
+
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
1390
|
+
titleIndex.set(normalizeTitleKey(fact.title), { id, okf_type: normalized.okf_type });
|
|
1391
|
+
if (normalized.edges.length > 0) {
|
|
1392
|
+
pendingEdges.push({ sourceId: id, sourceType: normalized.okf_type, edges: normalized.edges });
|
|
1393
|
+
}
|
|
1261
1394
|
}
|
|
1262
1395
|
}
|
|
1396
|
+
for (const item of pendingEdges) {
|
|
1397
|
+
await this.ontologyService?.resolveAndPersistEdges(
|
|
1398
|
+
entityId,
|
|
1399
|
+
item.sourceId,
|
|
1400
|
+
item.sourceType,
|
|
1401
|
+
item.edges ?? [],
|
|
1402
|
+
manifest,
|
|
1403
|
+
titleIndex,
|
|
1404
|
+
tx,
|
|
1405
|
+
now
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
});
|
|
1409
|
+
} catch (err) {
|
|
1410
|
+
const sqliteCode = err instanceof WikiTransactionError ? err.sqliteErrorCode : extractSqliteCode(err);
|
|
1411
|
+
if (sqliteCode !== "SQLITE_CONSTRAINT_UNIQUE") throw err;
|
|
1412
|
+
const canonical = await this.sourceRefIndexRepo.findActiveByEntityAndHash(entityId, sourceHash);
|
|
1413
|
+
if (canonical === null) throw err;
|
|
1414
|
+
if (onDuplicateHash === "throw" || onDuplicateHash === "ingest") {
|
|
1415
|
+
throw new WikiDuplicateHashError({ canonical, sourceHash, entityId });
|
|
1263
1416
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
entityId,
|
|
1267
|
-
item.sourceId,
|
|
1268
|
-
item.sourceType,
|
|
1269
|
-
item.edges ?? [],
|
|
1270
|
-
manifest,
|
|
1271
|
-
titleIndex,
|
|
1272
|
-
tx,
|
|
1273
|
-
now
|
|
1274
|
-
);
|
|
1275
|
-
}
|
|
1276
|
-
});
|
|
1417
|
+
return { truncated: false, chunks: 0, duplicateOf: canonical };
|
|
1418
|
+
}
|
|
1277
1419
|
await this.searchService.sync(entityId);
|
|
1278
1420
|
const uniqueDeletedSourceFactIds = Array.from(new Set(deletedSourceFactIds));
|
|
1279
1421
|
for (const factId of uniqueDeletedSourceFactIds) {
|
|
@@ -1289,7 +1431,7 @@ var IngestionService = class {
|
|
|
1289
1431
|
this.searchService.evictCache(entityId);
|
|
1290
1432
|
return { truncated, chunks: chunks.length };
|
|
1291
1433
|
} finally {
|
|
1292
|
-
|
|
1434
|
+
releaseIngestLocks();
|
|
1293
1435
|
}
|
|
1294
1436
|
}
|
|
1295
1437
|
};
|
|
@@ -1576,11 +1718,12 @@ var HEAL_MAX_PROMPT_CHARS = 4e4;
|
|
|
1576
1718
|
var HEAL_BATCH_SIZE = 25;
|
|
1577
1719
|
var HEAL_RECHECK_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1578
1720
|
var MaintenanceService = class {
|
|
1579
|
-
constructor(db, prefix, options, entryRepo, taskRepo, eventRepo, metadataRepo, searchService, jobManager, embeddingService, promptService, ontologyService) {
|
|
1721
|
+
constructor(db, prefix, options, entryRepo, sourceRefIndexRepo, taskRepo, eventRepo, metadataRepo, searchService, jobManager, embeddingService, promptService, ontologyService) {
|
|
1580
1722
|
this.db = db;
|
|
1581
1723
|
this.prefix = prefix;
|
|
1582
1724
|
this.options = options;
|
|
1583
1725
|
this.entryRepo = entryRepo;
|
|
1726
|
+
this.sourceRefIndexRepo = sourceRefIndexRepo;
|
|
1584
1727
|
this.taskRepo = taskRepo;
|
|
1585
1728
|
this.eventRepo = eventRepo;
|
|
1586
1729
|
this.metadataRepo = metadataRepo;
|
|
@@ -1739,10 +1882,13 @@ var MaintenanceService = class {
|
|
|
1739
1882
|
this.jobManager.releaseLock(op, entityId ?? "*");
|
|
1740
1883
|
}
|
|
1741
1884
|
}
|
|
1742
|
-
async forget(entityId, params) {
|
|
1885
|
+
async forget(entityId, params, opts) {
|
|
1743
1886
|
if (params.clearAll && (params.entryId !== void 0 || params.taskId !== void 0 || params.sourceRef !== void 0 || params.sourceHash !== void 0)) {
|
|
1744
1887
|
throw new Error("forget() clearAll is mutually exclusive with entryId, taskId, sourceRef, and sourceHash");
|
|
1745
1888
|
}
|
|
1889
|
+
if (opts?.dryRun === true) {
|
|
1890
|
+
return this.forgetDryRun(entityId, params);
|
|
1891
|
+
}
|
|
1746
1892
|
this.jobManager.acquireLock("forget", entityId);
|
|
1747
1893
|
try {
|
|
1748
1894
|
const now = Date.now();
|
|
@@ -1776,10 +1922,12 @@ var MaintenanceService = class {
|
|
|
1776
1922
|
const entryPromise = params.entryId ? this.entryRepo.softDelete(params.entryId, entityId, tx).then((r) => r.changes > 0) : null;
|
|
1777
1923
|
const taskDeletedPromise = params.taskId ? this.taskRepo.softDeleteById(params.taskId, entityId, tx).then((r) => r.changes > 0) : null;
|
|
1778
1924
|
const refPromise = sourceRef || sourceHash ? this.entryRepo.softDeleteBySource(entityId, tx, sourceRef, sourceHash) : null;
|
|
1925
|
+
const sourceRefIndexPromise = sourceRef ? this.sourceRefIndexRepo.softDeleteByEntityAndSourceRef(entityId, sourceRef, tx).then(() => void 0) : null;
|
|
1779
1926
|
const [entryResult, taskResult, refResult] = await Promise.all([
|
|
1780
1927
|
entryPromise ?? Promise.resolve(false),
|
|
1781
1928
|
taskDeletedPromise ?? Promise.resolve(false),
|
|
1782
|
-
refPromise ?? Promise.resolve(0)
|
|
1929
|
+
refPromise ?? Promise.resolve(0),
|
|
1930
|
+
sourceRefIndexPromise ?? Promise.resolve(void 0)
|
|
1783
1931
|
]);
|
|
1784
1932
|
if (entryResult) deletedEntries++;
|
|
1785
1933
|
if (taskResult) deletedTasks++;
|
|
@@ -1803,11 +1951,42 @@ var MaintenanceService = class {
|
|
|
1803
1951
|
throw new Error(`forget(${entityId}/${factId}) failed: ANN cleanup hook rejected`, { cause: this._sanitizeRankerError(hookErr) });
|
|
1804
1952
|
}
|
|
1805
1953
|
}
|
|
1806
|
-
return { deleted: { entries: deletedEntries, tasks: deletedTasks } };
|
|
1954
|
+
return params.clearAll ? { deleted: { entries: deletedEntries, tasks: deletedTasks }, metadataReset: true } : { deleted: { entries: deletedEntries, tasks: deletedTasks } };
|
|
1807
1955
|
} finally {
|
|
1808
1956
|
this.jobManager.releaseLock("forget", entityId);
|
|
1809
1957
|
}
|
|
1810
1958
|
}
|
|
1959
|
+
/**
|
|
1960
|
+
* Read-only dry-run preview of {@link forget}. Returns the same shape as a
|
|
1961
|
+
* real call would, without acquiring the lock, opening a transaction, staging
|
|
1962
|
+
* outbox events, or firing embedding hooks. Counts may be off-by-N during a
|
|
1963
|
+
* concurrent real forget — this is accepted, not considered a bug.
|
|
1964
|
+
*
|
|
1965
|
+
* `metadataReset` mirrors the real call: only `clearAll: true` returns true,
|
|
1966
|
+
* because only `clearAll` actually resets the metadata checkpoint.
|
|
1967
|
+
* `standard` (sourceRef/sourceHash) returns no metadataReset field, just like
|
|
1968
|
+
* the real call. `entryId`/`taskId` selectors are rejected with a clear error
|
|
1969
|
+
* (the spec keeps dry-run narrow).
|
|
1970
|
+
*/
|
|
1971
|
+
async forgetDryRun(entityId, params) {
|
|
1972
|
+
if (params.entryId !== void 0 || params.taskId !== void 0) {
|
|
1973
|
+
throw new Error("forget({ dryRun: true }) does not support entryId/taskId selectors; use sourceRef/sourceHash or clearAll");
|
|
1974
|
+
}
|
|
1975
|
+
if (params.clearAll) {
|
|
1976
|
+
const entries2 = await this.entryRepo.countLiveByEntityId(entityId);
|
|
1977
|
+
const tasks = await this.taskRepo.countLiveByEntityId(entityId);
|
|
1978
|
+
return { deleted: { entries: entries2, tasks }, metadataReset: true };
|
|
1979
|
+
}
|
|
1980
|
+
const sourceRef = params.sourceRef !== void 0 ? normalizeSourceRef(params.sourceRef) : null;
|
|
1981
|
+
if (params.sourceRef !== void 0 && !sourceRef) throw new Error("Invalid sourceRef");
|
|
1982
|
+
const sourceHash = params.sourceHash !== void 0 ? normalizeSourceHash(params.sourceHash) : null;
|
|
1983
|
+
if (params.sourceHash !== void 0 && !sourceHash) throw new Error("Invalid sourceHash (must be 64-char hex string)");
|
|
1984
|
+
if (sourceRef === null && sourceHash === null) {
|
|
1985
|
+
return { deleted: { entries: 0, tasks: 0 } };
|
|
1986
|
+
}
|
|
1987
|
+
const entries = await this.entryRepo.countLiveBySource(entityId, sourceRef, sourceHash);
|
|
1988
|
+
return { deleted: { entries, tasks: 0 } };
|
|
1989
|
+
}
|
|
1811
1990
|
/** Core librarian pass (locks handled by {@link runLibrarian}). Package-internal orchestration hook. */
|
|
1812
1991
|
async doRunLibrarian(entityId, promptOverride) {
|
|
1813
1992
|
const events = await this.eventRepo.getRecent(entityId, 50);
|
|
@@ -2531,6 +2710,15 @@ var ImportExportService = class {
|
|
|
2531
2710
|
const safeTitle = clip(String(fact.title ?? ""), IMPORT_TITLE_MAX);
|
|
2532
2711
|
const safeBody = clip(String(fact.body ?? ""), IMPORT_BODY_MAX);
|
|
2533
2712
|
clippedTextByFactId.set(fact.id, { title: safeTitle, body: safeBody });
|
|
2713
|
+
let normalizedSourceRef = null;
|
|
2714
|
+
if (fact.source_ref !== null && fact.source_ref !== void 0) {
|
|
2715
|
+
normalizedSourceRef = normalizeSourceRef(fact.source_ref);
|
|
2716
|
+
if (normalizedSourceRef === null) {
|
|
2717
|
+
throw new Error(
|
|
2718
|
+
`importDump: invalid source_ref ${JSON.stringify(fact.source_ref)} for entity "${entityId}" fact "${fact.id}" (must normalize to a non-empty string; see ingestDocument's sourceRef validation)`
|
|
2719
|
+
);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2534
2722
|
const factObj = {
|
|
2535
2723
|
id: fact.id,
|
|
2536
2724
|
entity_id: entityId,
|
|
@@ -2540,7 +2728,7 @@ var ImportExportService = class {
|
|
|
2540
2728
|
confidence: fact.confidence,
|
|
2541
2729
|
source_type: sourceType,
|
|
2542
2730
|
source_hash: fact.source_hash,
|
|
2543
|
-
source_ref:
|
|
2731
|
+
source_ref: normalizedSourceRef,
|
|
2544
2732
|
created_at: fact.created_at,
|
|
2545
2733
|
updated_at: safeUpdatedAt,
|
|
2546
2734
|
last_accessed_at: fact.last_accessed_at,
|
|
@@ -3553,6 +3741,6 @@ var WriteService = class {
|
|
|
3553
3741
|
}
|
|
3554
3742
|
};
|
|
3555
3743
|
|
|
3556
|
-
export { BaseRepository, DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, EmbeddingService, HEAL_BATCH_SIZE, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ImportExportService, IngestionService, JobManager, MaintenanceService, MetadataRepository, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, RetrievalService, SearchService, WikiBusyError, WikiTransactionError, WriteService, __privateAdd, __privateGet, __privateSet, chunkText, configureRandomSource, emptyManifest, entitySummaryMetaKey, extractSqliteCode, generateId, normalizeSourceHash, normalizeSourceRef, normalizeTitleKey, parseEmbedding, resolveEdgeDefinitions, resolveNodeType, safeSlice, validateInlineEdges, validateManifest };
|
|
3557
|
-
//# sourceMappingURL=chunk-
|
|
3558
|
-
//# sourceMappingURL=chunk-
|
|
3744
|
+
export { BaseRepository, DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, EmbeddingService, HEAL_BATCH_SIZE, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ImportExportService, IngestionService, JobManager, MaintenanceService, MetadataRepository, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, RetrievalService, SearchService, WikiBusyError, WikiDuplicateHashError, WikiTransactionError, WriteService, __privateAdd, __privateGet, __privateSet, chunkText, configureRandomSource, emptyManifest, entitySummaryMetaKey, extractSqliteCode, generateId, normalizeSourceHash, normalizeSourceRef, normalizeTitleKey, parseEmbedding, resolveEdgeDefinitions, resolveNodeType, safeSlice, validateInlineEdges, validateManifest };
|
|
3745
|
+
//# sourceMappingURL=chunk-6HNIOKT4.mjs.map
|
|
3746
|
+
//# sourceMappingURL=chunk-6HNIOKT4.mjs.map
|