@equationalapplications/core-llm-wiki 5.2.0 → 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-J3N3WRK7.mjs → chunk-6HNIOKT4.mjs} +188 -77
- 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 +326 -85
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +142 -12
- package/dist/index.mjs.map +1 -1
- package/dist/{testing-DyXISRwS.d.mts → testing-CAk9oFvw.d.mts} +109 -6
- package/dist/{testing-DyXISRwS.d.ts → testing-CAk9oFvw.d.ts} +109 -6
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +240 -75
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +1 -1
- package/package.json +2 -2
- package/dist/chunk-J3N3WRK7.mjs.map +0 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { M as MemoryBundle, F as FormatContextOptions, G as GraphNeighborhood, a as MemoryDump, b as FormattedMemoryDump, O as OntologyManifest, R as ReadOptions, S as SQLiteAdapter, W as WikiOptions, c as WikiMemory } from './testing-
|
|
2
|
-
export { E as EntityStatus, d as ExtractedFact, e as ExtractedFactEdge, f as ExtractedFactWithOntology, g as ExtractedTask, h as GraphTraversalOptions, H as HEAL_BATCH_SIZE, i as HEAL_RECHECK_MS, j as HOOK_TIMEOUT_MARKER, k as HealResult, L as LLMProvider, l as ONTOLOGY_BACKFILL_BATCH_SIZE, m as ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, n as ONTOLOGY_BACKFILL_RECHECK_MS, o as OntologyBackfillResult, p as OntologyConfig, q as OntologyEdgeType, r as OntologyMode, s as OntologyNodeType, t as OntologyPromptContext, u as OntologyUpdates, P as PromptOverrides, v as PromptService, w as PrunePartialFailureError, V as VectorRanker, x as VectorRankerFallback, y as VectorRankerRankArgs, z as VectorRankerSemanticResult, A as WikiBusyError, B as WikiBusyOperation, C as WikiCheckpoint, D as WikiConfig, I as WikiDuplicateHashError, J as WikiEdge, K as WikiEvent, N as WikiFact, Q as WikiMemoryTestAccess, T as WikiOutboxEvent, U as WikiTask, X as WikiTransactionError } from './testing-
|
|
1
|
+
import { M as MemoryBundle, F as FormatContextOptions, G as GraphNeighborhood, a as MemoryDump, b as FormattedMemoryDump, O as OntologyManifest, R as ReadOptions, S as SQLiteAdapter, W as WikiOptions, c as WikiMemory } from './testing-CAk9oFvw.mjs';
|
|
2
|
+
export { E as EntityStatus, d as ExtractedFact, e as ExtractedFactEdge, f as ExtractedFactWithOntology, g as ExtractedTask, h as GraphTraversalOptions, H as HEAL_BATCH_SIZE, i as HEAL_RECHECK_MS, j as HOOK_TIMEOUT_MARKER, k as HealResult, L as LLMProvider, l as ONTOLOGY_BACKFILL_BATCH_SIZE, m as ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, n as ONTOLOGY_BACKFILL_RECHECK_MS, o as OntologyBackfillResult, p as OntologyConfig, q as OntologyEdgeType, r as OntologyMode, s as OntologyNodeType, t as OntologyPromptContext, u as OntologyUpdates, P as PromptOverrides, v as PromptService, w as PrunePartialFailureError, V as VectorRanker, x as VectorRankerFallback, y as VectorRankerRankArgs, z as VectorRankerSemanticResult, A as WikiBusyError, B as WikiBusyOperation, C as WikiCheckpoint, D as WikiConfig, I as WikiDuplicateHashError, J as WikiEdge, K as WikiEvent, N as WikiFact, Q as WikiMemoryTestAccess, T as WikiOutboxEvent, U as WikiTask, X as WikiTransactionError } from './testing-CAk9oFvw.mjs';
|
|
3
3
|
import { OkfFile } from '@equationalapplications/core-okf';
|
|
4
4
|
import 'minisearch';
|
|
5
5
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { M as MemoryBundle, F as FormatContextOptions, G as GraphNeighborhood, a as MemoryDump, b as FormattedMemoryDump, O as OntologyManifest, R as ReadOptions, S as SQLiteAdapter, W as WikiOptions, c as WikiMemory } from './testing-
|
|
2
|
-
export { E as EntityStatus, d as ExtractedFact, e as ExtractedFactEdge, f as ExtractedFactWithOntology, g as ExtractedTask, h as GraphTraversalOptions, H as HEAL_BATCH_SIZE, i as HEAL_RECHECK_MS, j as HOOK_TIMEOUT_MARKER, k as HealResult, L as LLMProvider, l as ONTOLOGY_BACKFILL_BATCH_SIZE, m as ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, n as ONTOLOGY_BACKFILL_RECHECK_MS, o as OntologyBackfillResult, p as OntologyConfig, q as OntologyEdgeType, r as OntologyMode, s as OntologyNodeType, t as OntologyPromptContext, u as OntologyUpdates, P as PromptOverrides, v as PromptService, w as PrunePartialFailureError, V as VectorRanker, x as VectorRankerFallback, y as VectorRankerRankArgs, z as VectorRankerSemanticResult, A as WikiBusyError, B as WikiBusyOperation, C as WikiCheckpoint, D as WikiConfig, I as WikiDuplicateHashError, J as WikiEdge, K as WikiEvent, N as WikiFact, Q as WikiMemoryTestAccess, T as WikiOutboxEvent, U as WikiTask, X as WikiTransactionError } from './testing-
|
|
1
|
+
import { M as MemoryBundle, F as FormatContextOptions, G as GraphNeighborhood, a as MemoryDump, b as FormattedMemoryDump, O as OntologyManifest, R as ReadOptions, S as SQLiteAdapter, W as WikiOptions, c as WikiMemory } from './testing-CAk9oFvw.js';
|
|
2
|
+
export { E as EntityStatus, d as ExtractedFact, e as ExtractedFactEdge, f as ExtractedFactWithOntology, g as ExtractedTask, h as GraphTraversalOptions, H as HEAL_BATCH_SIZE, i as HEAL_RECHECK_MS, j as HOOK_TIMEOUT_MARKER, k as HealResult, L as LLMProvider, l as ONTOLOGY_BACKFILL_BATCH_SIZE, m as ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, n as ONTOLOGY_BACKFILL_RECHECK_MS, o as OntologyBackfillResult, p as OntologyConfig, q as OntologyEdgeType, r as OntologyMode, s as OntologyNodeType, t as OntologyPromptContext, u as OntologyUpdates, P as PromptOverrides, v as PromptService, w as PrunePartialFailureError, V as VectorRanker, x as VectorRankerFallback, y as VectorRankerRankArgs, z as VectorRankerSemanticResult, A as WikiBusyError, B as WikiBusyOperation, C as WikiCheckpoint, D as WikiConfig, I as WikiDuplicateHashError, J as WikiEdge, K as WikiEvent, N as WikiFact, Q as WikiMemoryTestAccess, T as WikiOutboxEvent, U as WikiTask, X as WikiTransactionError } from './testing-CAk9oFvw.js';
|
|
3
3
|
import { OkfFile } from '@equationalapplications/core-okf';
|
|
4
4
|
import 'minisearch';
|
|
5
5
|
|
package/dist/index.js
CHANGED
|
@@ -45,6 +45,24 @@ async function setupDatabase(db, prefix) {
|
|
|
45
45
|
CREATE INDEX IF NOT EXISTS ${prefix}entries_source_hash_idx ON ${prefix}entries(entity_id, source_hash) WHERE source_hash IS NOT NULL;
|
|
46
46
|
CREATE INDEX IF NOT EXISTS ${prefix}entries_updated_idx ON ${prefix}entries(updated_at DESC);
|
|
47
47
|
|
|
48
|
+
-- source_ref_index: per-(entity, source_hash) record of the canonical sourceRef
|
|
49
|
+
-- currently holding that hash. The partial UNIQUE index on (entity_id, source_hash)
|
|
50
|
+
-- WHERE deleted_at IS NULL enforces the sourceRef-level TOCTOU-race invariant;
|
|
51
|
+
-- entries-level uniqueness cannot express it because a single ingestDocument call
|
|
52
|
+
-- writes N facts that all share (entity_id, source_ref, source_hash). See
|
|
53
|
+
-- docs/superpowers/specs/2026-08-07-dependabot-concurrency-release-hygiene-design.md \xA7B1.
|
|
54
|
+
CREATE TABLE IF NOT EXISTS ${prefix}source_ref_index (
|
|
55
|
+
id TEXT PRIMARY KEY,
|
|
56
|
+
entity_id TEXT NOT NULL,
|
|
57
|
+
source_hash TEXT NOT NULL,
|
|
58
|
+
source_ref TEXT NOT NULL,
|
|
59
|
+
created_at INTEGER NOT NULL,
|
|
60
|
+
deleted_at INTEGER
|
|
61
|
+
);
|
|
62
|
+
CREATE UNIQUE INDEX IF NOT EXISTS ${prefix}idx_source_ref_hash
|
|
63
|
+
ON ${prefix}source_ref_index (entity_id, source_hash)
|
|
64
|
+
WHERE deleted_at IS NULL;
|
|
65
|
+
|
|
48
66
|
CREATE TABLE IF NOT EXISTS ${prefix}tasks (
|
|
49
67
|
id TEXT PRIMARY KEY,
|
|
50
68
|
entity_id TEXT NOT NULL,
|
|
@@ -149,6 +167,12 @@ var SQLITE_RESULT_CODE_NAMES = {
|
|
|
149
167
|
26: "SQLITE_NOTADB",
|
|
150
168
|
27: "SQLITE_NOTICE",
|
|
151
169
|
28: "SQLITE_WARNING",
|
|
170
|
+
// Extended result codes (https://sqlite.org/rescode.html#extended_result_code_list).
|
|
171
|
+
// 2067 = SQLITE_CONSTRAINT_UNIQUE — raised when a UNIQUE index/PRIMARY KEY
|
|
172
|
+
// rejects an INSERT/UPDATE; expo-sqlite reports this as a numeric "Error code N:"
|
|
173
|
+
// prefix and we normalize it back to the symbolic name so callers can branch on
|
|
174
|
+
// the same string the rest of the ecosystem uses.
|
|
175
|
+
2067: "SQLITE_CONSTRAINT_UNIQUE",
|
|
152
176
|
100: "SQLITE_ROW",
|
|
153
177
|
101: "SQLITE_DONE"
|
|
154
178
|
};
|
|
@@ -377,6 +401,61 @@ var MIGRATIONS = [
|
|
|
377
401
|
);
|
|
378
402
|
}
|
|
379
403
|
}
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
version: 9,
|
|
407
|
+
description: "add_source_ref_index",
|
|
408
|
+
run: async (db, prefix) => {
|
|
409
|
+
const duplicates = await db.getAllAsync(
|
|
410
|
+
`SELECT entity_id, source_hash, COUNT(DISTINCT source_ref) AS n_refs
|
|
411
|
+
FROM ${prefix}entries
|
|
412
|
+
WHERE deleted_at IS NULL AND source_hash IS NOT NULL
|
|
413
|
+
GROUP BY entity_id, source_hash
|
|
414
|
+
HAVING COUNT(DISTINCT source_ref) > 1`
|
|
415
|
+
);
|
|
416
|
+
if (duplicates.length > 0) {
|
|
417
|
+
const sample = duplicates.slice(0, 5).map((d) => `(entity_id=${d.entity_id}, source_hash=${d.source_hash.slice(0, 12)}\u2026, n_refs=${d.n_refs})`).join(", ");
|
|
418
|
+
throw new Error(
|
|
419
|
+
`Migration v9 (add_source_ref_index) failed: existing live rows have multiple sourceRefs sharing a hash. Found ${duplicates.length} duplicate (entity_id, source_hash) groups. First ${Math.min(5, duplicates.length)}: ${sample}. Resolve each by calling forget({ sourceRef: <loser> }) for the offending sourceRef, then re-run setup.`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
await db.execAsync(
|
|
423
|
+
`CREATE TABLE IF NOT EXISTS ${prefix}source_ref_index (
|
|
424
|
+
id TEXT PRIMARY KEY,
|
|
425
|
+
entity_id TEXT NOT NULL,
|
|
426
|
+
source_hash TEXT NOT NULL,
|
|
427
|
+
source_ref TEXT NOT NULL,
|
|
428
|
+
created_at INTEGER NOT NULL,
|
|
429
|
+
deleted_at INTEGER
|
|
430
|
+
);
|
|
431
|
+
CREATE UNIQUE INDEX IF NOT EXISTS ${prefix}idx_source_ref_hash
|
|
432
|
+
ON ${prefix}source_ref_index (entity_id, source_hash)
|
|
433
|
+
WHERE deleted_at IS NULL;`
|
|
434
|
+
);
|
|
435
|
+
await db.execAsync(
|
|
436
|
+
`INSERT OR IGNORE INTO ${prefix}source_ref_index (id, entity_id, source_hash, source_ref, created_at, deleted_at)
|
|
437
|
+
SELECT
|
|
438
|
+
'sri:' || entity_id || ':' || source_hash,
|
|
439
|
+
entity_id,
|
|
440
|
+
source_hash,
|
|
441
|
+
source_ref,
|
|
442
|
+
updated_at,
|
|
443
|
+
NULL
|
|
444
|
+
FROM (
|
|
445
|
+
SELECT
|
|
446
|
+
entity_id, source_hash, source_ref, updated_at,
|
|
447
|
+
ROW_NUMBER() OVER (
|
|
448
|
+
PARTITION BY entity_id, source_hash
|
|
449
|
+
ORDER BY updated_at ASC, id ASC
|
|
450
|
+
) AS rn
|
|
451
|
+
FROM ${prefix}entries
|
|
452
|
+
WHERE deleted_at IS NULL
|
|
453
|
+
AND source_hash IS NOT NULL
|
|
454
|
+
AND source_ref IS NOT NULL
|
|
455
|
+
) ranked
|
|
456
|
+
WHERE rn = 1;`
|
|
457
|
+
);
|
|
458
|
+
}
|
|
380
459
|
}
|
|
381
460
|
];
|
|
382
461
|
for (let i = 1; i < MIGRATIONS.length; i++) {
|
|
@@ -1532,6 +1611,57 @@ var OutboxRepository = class extends BaseRepository {
|
|
|
1532
1611
|
}
|
|
1533
1612
|
};
|
|
1534
1613
|
|
|
1614
|
+
// src/repositories/SourceRefIndexRepository.ts
|
|
1615
|
+
var SourceRefIndexRepository = class extends BaseRepository {
|
|
1616
|
+
/**
|
|
1617
|
+
* Idempotent insert: the partial UNIQUE index catches concurrent inserts for
|
|
1618
|
+
* the same (entity_id, source_hash). The caller (IngestionService) catches
|
|
1619
|
+
* the resulting SQLITE_CONSTRAINT_UNIQUE and translates it to the per-mode
|
|
1620
|
+
* duplicate-hash outcome. Runtime IDs use the `sri_` prefix to avoid
|
|
1621
|
+
* collision with the deterministic `sri:<entity>:<hash>` IDs used by the v9
|
|
1622
|
+
* backfill (the colon separator keeps the two ID spaces disjoint).
|
|
1623
|
+
*/
|
|
1624
|
+
async upsert(entityId, sourceHash, sourceRef, tx) {
|
|
1625
|
+
const executor = this.getExecutor(tx);
|
|
1626
|
+
await executor.runAsync(
|
|
1627
|
+
`INSERT INTO ${this.prefix}source_ref_index (id, entity_id, source_hash, source_ref, created_at, deleted_at)
|
|
1628
|
+
VALUES (?, ?, ?, ?, ?, NULL)`,
|
|
1629
|
+
[generateId("sri_"), entityId, sourceHash, sourceRef, Date.now()]
|
|
1630
|
+
);
|
|
1631
|
+
}
|
|
1632
|
+
/**
|
|
1633
|
+
* Idempotent soft-delete of the live row for (entity_id, source_ref).
|
|
1634
|
+
* Called at the start of every ingestDocument to remove the prior run's
|
|
1635
|
+
* index row, so the new upsert doesn't collide with itself. No-op when the
|
|
1636
|
+
* row is already soft-deleted or never existed.
|
|
1637
|
+
*/
|
|
1638
|
+
async softDeleteByEntityAndSourceRef(entityId, sourceRef, tx) {
|
|
1639
|
+
const executor = this.getExecutor(tx);
|
|
1640
|
+
const now = Date.now();
|
|
1641
|
+
const result = await executor.runAsync(
|
|
1642
|
+
`UPDATE ${this.prefix}source_ref_index
|
|
1643
|
+
SET deleted_at = ?, created_at = ?
|
|
1644
|
+
WHERE entity_id = ? AND source_ref = ? AND deleted_at IS NULL`,
|
|
1645
|
+
[now, now, entityId, sourceRef]
|
|
1646
|
+
);
|
|
1647
|
+
return result.changes;
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Returns the live sourceRef holding the given hash, or null when no live
|
|
1651
|
+
* row exists. Used by the IngestionService pre-check (line 82) and the
|
|
1652
|
+
* catch-and-translate canonical lookup (line 212).
|
|
1653
|
+
*/
|
|
1654
|
+
async findActiveByEntityAndHash(entityId, sourceHash, tx) {
|
|
1655
|
+
const executor = this.getExecutor(tx);
|
|
1656
|
+
const row = await executor.getFirstAsync(
|
|
1657
|
+
`SELECT source_ref FROM ${this.prefix}source_ref_index
|
|
1658
|
+
WHERE entity_id = ? AND source_hash = ? AND deleted_at IS NULL`,
|
|
1659
|
+
[entityId, sourceHash]
|
|
1660
|
+
);
|
|
1661
|
+
return row?.source_ref ?? null;
|
|
1662
|
+
}
|
|
1663
|
+
};
|
|
1664
|
+
|
|
1535
1665
|
// src/repositories/TaskRepository.ts
|
|
1536
1666
|
function mapRowToTask(row) {
|
|
1537
1667
|
return {
|
|
@@ -2575,6 +2705,19 @@ var JobManager = class {
|
|
|
2575
2705
|
this.prefix = prefix;
|
|
2576
2706
|
this.activeMaintenanceJobs = /* @__PURE__ */ new Set();
|
|
2577
2707
|
this.activeIngestJobs = /* @__PURE__ */ new Map();
|
|
2708
|
+
/**
|
|
2709
|
+
* Per-(entityId, sourceHash) promise-chain lock. Serializes ingest races
|
|
2710
|
+
* at the application level (the v9 partial UNIQUE index enforces it at
|
|
2711
|
+
* the DB level as defense-in-depth). Two callers holding the same hash
|
|
2712
|
+
* see FIFO order; different hashes never block one another.
|
|
2713
|
+
*
|
|
2714
|
+
* Keyed `${entityId}\0${sourceHash}` so distinct (entityId, sourceHash)
|
|
2715
|
+
* pairs can never share a lock. A raw concatenation would let
|
|
2716
|
+
* ("ab", "c…") collide with ("a", "bc…"); the NUL separator makes that
|
|
2717
|
+
* impossible because neither component can contain NUL (entityIds are
|
|
2718
|
+
* user-supplied identifiers; sourceHashes are hex digests).
|
|
2719
|
+
*/
|
|
2720
|
+
this.hashLocks = /* @__PURE__ */ new Map();
|
|
2578
2721
|
this.statusSubscribers = /* @__PURE__ */ new Map();
|
|
2579
2722
|
/**
|
|
2580
2723
|
* Lookup table for acquireLock/releaseLock's dynamic-dispatch branch.
|
|
@@ -2592,6 +2735,51 @@ var JobManager = class {
|
|
|
2592
2735
|
ontologyBackfill: (id) => this._ontologyBackfillKey(id)
|
|
2593
2736
|
};
|
|
2594
2737
|
}
|
|
2738
|
+
_hashLockKey(entityId, sourceHash) {
|
|
2739
|
+
return `${entityId}\0${sourceHash}`;
|
|
2740
|
+
}
|
|
2741
|
+
/**
|
|
2742
|
+
* Acquire a promise-chain lock scoped to one `(entityId, sourceHash)` pair.
|
|
2743
|
+
* Returns a zero-argument release closure that removes the current tail
|
|
2744
|
+
* from the chain. Releasing is idempotent so a holder that double-fires
|
|
2745
|
+
* (e.g. on a finally that races an explicit release) cannot unblock
|
|
2746
|
+
* the next holder prematurely.
|
|
2747
|
+
*
|
|
2748
|
+
* Documented ordering: ingest acquires the hash lock FIRST, then the
|
|
2749
|
+
* synchronous sourceRef ingest lock, and the release closure unwinds in
|
|
2750
|
+
* the opposite order (sourceRef first, hash second). Hash-then-sourceRef
|
|
2751
|
+
* is required because the v9 partial UNIQUE index conflicts on the
|
|
2752
|
+
* (entity_id, source_hash) pair regardless of source_ref, so two callers
|
|
2753
|
+
* with DIFFERENT source_refs racing the same hash are the exact case the
|
|
2754
|
+
* TOCTOU race fix needs to serialize.
|
|
2755
|
+
*
|
|
2756
|
+
* Mechanics: each holder owns a private `released` promise that resolves
|
|
2757
|
+
* only when THIS holder calls release(). The map stores the holder's
|
|
2758
|
+
* `released` promise as the "tail" — the NEXT caller awaits that tail
|
|
2759
|
+
* via `previous = this.hashLocks.get(key)`. The current caller gets
|
|
2760
|
+
* their unique release function once `previous` (the prior holder's
|
|
2761
|
+
* release signal) settles, so each holder is guaranteed a UNIQUE release
|
|
2762
|
+
* closure bound to their own `released` promise.
|
|
2763
|
+
*/
|
|
2764
|
+
acquireHashLock(entityId, sourceHash) {
|
|
2765
|
+
const key = this._hashLockKey(entityId, sourceHash);
|
|
2766
|
+
const previous = this.hashLocks.get(key) ?? Promise.resolve();
|
|
2767
|
+
let resolveReleased;
|
|
2768
|
+
const released = new Promise((resolve) => {
|
|
2769
|
+
resolveReleased = resolve;
|
|
2770
|
+
});
|
|
2771
|
+
let fired = false;
|
|
2772
|
+
const release = () => {
|
|
2773
|
+
if (fired) return;
|
|
2774
|
+
fired = true;
|
|
2775
|
+
if (this.hashLocks.get(key) === released) {
|
|
2776
|
+
this.hashLocks.delete(key);
|
|
2777
|
+
}
|
|
2778
|
+
resolveReleased();
|
|
2779
|
+
};
|
|
2780
|
+
this.hashLocks.set(key, released);
|
|
2781
|
+
return previous.then(() => release);
|
|
2782
|
+
}
|
|
2595
2783
|
_pruneKey(entityId) {
|
|
2596
2784
|
return `${this.prefix}:${entityId}:prune`;
|
|
2597
2785
|
}
|
|
@@ -2805,6 +2993,38 @@ var JobManager = class {
|
|
|
2805
2993
|
this._notifyStatusSubscribers(entityId);
|
|
2806
2994
|
return true;
|
|
2807
2995
|
}
|
|
2996
|
+
/**
|
|
2997
|
+
* Centralized ingest lock acquisition. Acquires the hash lock FIRST
|
|
2998
|
+
* (FIFO across callers racing the same hash), then the synchronous
|
|
2999
|
+
* sourceRef ingest lock (rejects cross-entity conflicts and busy
|
|
3000
|
+
* operations). Returns a single zero-argument release closure that
|
|
3001
|
+
* unwinds in the opposite order — sourceRef first, hash second — so
|
|
3002
|
+
* the sourceRef lock is freed the instant its work is done and only
|
|
3003
|
+
* the hash lock keeps serializing the duplicate-content race window.
|
|
3004
|
+
*
|
|
3005
|
+
* On a sourceRef conflict (WikiBusyError thrown synchronously by the
|
|
3006
|
+
* underlying acquireLock) the hash lock is released BEFORE the error
|
|
3007
|
+
* propagates, so the next caller in the hash chain is not held up by
|
|
3008
|
+
* a request that never reached the DB.
|
|
3009
|
+
*/
|
|
3010
|
+
async acquireIngestLocks(entityId, sourceRef, sourceHash) {
|
|
3011
|
+
const releaseHash = await this.acquireHashLock(entityId, sourceHash);
|
|
3012
|
+
let sourceLockAcquired = false;
|
|
3013
|
+
try {
|
|
3014
|
+
this.acquireLock("ingest", entityId, sourceRef);
|
|
3015
|
+
sourceLockAcquired = true;
|
|
3016
|
+
} catch (err) {
|
|
3017
|
+
releaseHash();
|
|
3018
|
+
throw err;
|
|
3019
|
+
}
|
|
3020
|
+
return () => {
|
|
3021
|
+
try {
|
|
3022
|
+
if (sourceLockAcquired) this.releaseLock("ingest", entityId, sourceRef);
|
|
3023
|
+
} finally {
|
|
3024
|
+
releaseHash();
|
|
3025
|
+
}
|
|
3026
|
+
};
|
|
3027
|
+
}
|
|
2808
3028
|
/**
|
|
2809
3029
|
* Validates then acquires global + per-entity import locks atomically.
|
|
2810
3030
|
* Validates all entities before acquiring any lock (same as current importDump semantics).
|
|
@@ -3245,11 +3465,12 @@ var DEFAULT_CHUNK_OVERLAP = 400;
|
|
|
3245
3465
|
|
|
3246
3466
|
// src/services/IngestionService.ts
|
|
3247
3467
|
var IngestionService = class {
|
|
3248
|
-
constructor(db, prefix, options, entryRepo, searchService, jobManager, embeddingService, promptService, ontologyService) {
|
|
3468
|
+
constructor(db, prefix, options, entryRepo, sourceRefIndexRepo, searchService, jobManager, embeddingService, promptService, ontologyService) {
|
|
3249
3469
|
this.db = db;
|
|
3250
3470
|
this.prefix = prefix;
|
|
3251
3471
|
this.options = options;
|
|
3252
3472
|
this.entryRepo = entryRepo;
|
|
3473
|
+
this.sourceRefIndexRepo = sourceRefIndexRepo;
|
|
3253
3474
|
this.searchService = searchService;
|
|
3254
3475
|
this.jobManager = jobManager;
|
|
3255
3476
|
this.embeddingService = embeddingService;
|
|
@@ -3261,18 +3482,6 @@ var IngestionService = class {
|
|
|
3261
3482
|
if (!sourceRef) throw new Error("Invalid sourceRef");
|
|
3262
3483
|
const sourceHash = normalizeSourceHash(params.sourceHash);
|
|
3263
3484
|
if (!sourceHash) throw new Error("Invalid sourceHash (must be 64-char hex string)");
|
|
3264
|
-
const onDuplicateHash = opts?.onDuplicateHash ?? "ingest";
|
|
3265
|
-
if (onDuplicateHash !== "ingest") {
|
|
3266
|
-
const refs = await this.entryRepo.findSourceRefsByHash(entityId, sourceHash);
|
|
3267
|
-
const others = refs.filter((r) => r !== sourceRef);
|
|
3268
|
-
if (others.length > 0) {
|
|
3269
|
-
const canonical = others[0];
|
|
3270
|
-
if (onDuplicateHash === "throw") {
|
|
3271
|
-
throw new WikiDuplicateHashError({ canonical, sourceHash, entityId });
|
|
3272
|
-
}
|
|
3273
|
-
return { truncated: false, chunks: 0, duplicateOf: canonical };
|
|
3274
|
-
}
|
|
3275
|
-
}
|
|
3276
3485
|
const maxChunkLength = params.maxChunkLength ?? this.options.config?.maxChunkLength ?? DEFAULT_MAX_CHUNK_LENGTH;
|
|
3277
3486
|
const rawOverlap = params.chunkOverlap ?? this.options.config?.chunkOverlap ?? DEFAULT_CHUNK_OVERLAP;
|
|
3278
3487
|
const chunkOverlap = Math.min(
|
|
@@ -3284,8 +3493,18 @@ var IngestionService = class {
|
|
|
3284
3493
|
if (typeof params.documentChunk !== "string") {
|
|
3285
3494
|
throw new Error(`documentChunk must be a string, received ${typeof params.documentChunk}`);
|
|
3286
3495
|
}
|
|
3287
|
-
this.jobManager.
|
|
3496
|
+
const releaseIngestLocks = await this.jobManager.acquireIngestLocks(entityId, sourceRef, sourceHash);
|
|
3288
3497
|
try {
|
|
3498
|
+
const onDuplicateHash = opts?.onDuplicateHash ?? "ingest";
|
|
3499
|
+
if (onDuplicateHash !== "ingest") {
|
|
3500
|
+
const canonical = await this.sourceRefIndexRepo.findActiveByEntityAndHash(entityId, sourceHash);
|
|
3501
|
+
if (canonical !== null && canonical !== sourceRef) {
|
|
3502
|
+
if (onDuplicateHash === "throw") {
|
|
3503
|
+
throw new WikiDuplicateHashError({ canonical, sourceHash, entityId });
|
|
3504
|
+
}
|
|
3505
|
+
return { truncated: false, chunks: 0, duplicateOf: canonical };
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3289
3508
|
const { chunks, truncated } = chunkText(params.documentChunk, maxChunkLength, chunkOverlap);
|
|
3290
3509
|
if (chunks.length === 0) return { truncated: false, chunks: 0 };
|
|
3291
3510
|
const chunkResults = await withConcurrency(
|
|
@@ -3321,68 +3540,81 @@ var IngestionService = class {
|
|
|
3321
3540
|
const now = Date.now();
|
|
3322
3541
|
const insertedFacts = [];
|
|
3323
3542
|
const deletedSourceFactIds = [];
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
for (const { facts, ontology_updates } of orderedChunkFacts) {
|
|
3339
|
-
if (mode === "emergent" && ontology_updates && this.ontologyService) {
|
|
3340
|
-
manifest = await this.ontologyService.mergeEmergentUpdates(entityId, ontology_updates, tx);
|
|
3341
|
-
ontologyState = await this.ontologyService.getEffectiveState(entityId, tx);
|
|
3342
|
-
mode = ontologyState.mode;
|
|
3543
|
+
try {
|
|
3544
|
+
await this.db.withTransactionAsync(async (tx) => {
|
|
3545
|
+
deletedSourceFactIds.push(...await this.entryRepo.findIdsBySource(entityId, sourceRef, null, tx, false));
|
|
3546
|
+
await this.entryRepo.softDeleteBySource(entityId, tx, sourceRef, null);
|
|
3547
|
+
await this.sourceRefIndexRepo.softDeleteByEntityAndSourceRef(entityId, sourceRef, tx);
|
|
3548
|
+
await this.sourceRefIndexRepo.upsert(entityId, sourceHash, sourceRef, tx);
|
|
3549
|
+
const titleIndex = /* @__PURE__ */ new Map();
|
|
3550
|
+
const pendingEdges = [];
|
|
3551
|
+
const existingFacts = await this.entryRepo.findRecentByEntityId(entityId, 500, tx);
|
|
3552
|
+
for (const existing of existingFacts) {
|
|
3553
|
+
titleIndex.set(normalizeTitleKey(existing.title), {
|
|
3554
|
+
id: existing.id,
|
|
3555
|
+
okf_type: existing.okf_type ?? null
|
|
3556
|
+
});
|
|
3343
3557
|
}
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3558
|
+
let ontologyState = await this.ontologyService?.getEffectiveState(entityId, tx) ?? { mode: "off", manifest: { node_types: [], edge_types: [] } };
|
|
3559
|
+
let { mode, manifest } = ontologyState;
|
|
3560
|
+
for (const { facts, ontology_updates } of orderedChunkFacts) {
|
|
3561
|
+
if (mode === "emergent" && ontology_updates && this.ontologyService) {
|
|
3562
|
+
manifest = await this.ontologyService.mergeEmergentUpdates(entityId, ontology_updates, tx);
|
|
3563
|
+
ontologyState = await this.ontologyService.getEffectiveState(entityId, tx);
|
|
3564
|
+
mode = ontologyState.mode;
|
|
3565
|
+
}
|
|
3566
|
+
for (const fact of facts) {
|
|
3567
|
+
const ontologyFact = fact;
|
|
3568
|
+
const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest) ?? { okf_type: null, edges: [] };
|
|
3569
|
+
const id = generateId("fact_");
|
|
3570
|
+
const wikiFact = {
|
|
3571
|
+
id,
|
|
3572
|
+
entity_id: entityId,
|
|
3573
|
+
title: fact.title,
|
|
3574
|
+
body: fact.body,
|
|
3575
|
+
tags: fact.tags,
|
|
3576
|
+
confidence: fact.confidence,
|
|
3577
|
+
source_type: "immutable_document",
|
|
3578
|
+
source_hash: sourceHash,
|
|
3579
|
+
source_ref: sourceRef,
|
|
3580
|
+
created_at: now,
|
|
3581
|
+
updated_at: now,
|
|
3582
|
+
last_accessed_at: null,
|
|
3583
|
+
access_count: 0,
|
|
3584
|
+
deleted_at: null,
|
|
3585
|
+
okf_type: normalized.okf_type
|
|
3586
|
+
};
|
|
3587
|
+
await this.entryRepo.upsert(wikiFact, tx);
|
|
3588
|
+
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
3589
|
+
titleIndex.set(normalizeTitleKey(fact.title), { id, okf_type: normalized.okf_type });
|
|
3590
|
+
if (normalized.edges.length > 0) {
|
|
3591
|
+
pendingEdges.push({ sourceId: id, sourceType: normalized.okf_type, edges: normalized.edges });
|
|
3592
|
+
}
|
|
3370
3593
|
}
|
|
3371
3594
|
}
|
|
3595
|
+
for (const item of pendingEdges) {
|
|
3596
|
+
await this.ontologyService?.resolveAndPersistEdges(
|
|
3597
|
+
entityId,
|
|
3598
|
+
item.sourceId,
|
|
3599
|
+
item.sourceType,
|
|
3600
|
+
item.edges ?? [],
|
|
3601
|
+
manifest,
|
|
3602
|
+
titleIndex,
|
|
3603
|
+
tx,
|
|
3604
|
+
now
|
|
3605
|
+
);
|
|
3606
|
+
}
|
|
3607
|
+
});
|
|
3608
|
+
} catch (err) {
|
|
3609
|
+
const sqliteCode = err instanceof WikiTransactionError ? err.sqliteErrorCode : extractSqliteCode(err);
|
|
3610
|
+
if (sqliteCode !== "SQLITE_CONSTRAINT_UNIQUE") throw err;
|
|
3611
|
+
const canonical = await this.sourceRefIndexRepo.findActiveByEntityAndHash(entityId, sourceHash);
|
|
3612
|
+
if (canonical === null) throw err;
|
|
3613
|
+
if (onDuplicateHash === "throw" || onDuplicateHash === "ingest") {
|
|
3614
|
+
throw new WikiDuplicateHashError({ canonical, sourceHash, entityId });
|
|
3372
3615
|
}
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
entityId,
|
|
3376
|
-
item.sourceId,
|
|
3377
|
-
item.sourceType,
|
|
3378
|
-
item.edges ?? [],
|
|
3379
|
-
manifest,
|
|
3380
|
-
titleIndex,
|
|
3381
|
-
tx,
|
|
3382
|
-
now
|
|
3383
|
-
);
|
|
3384
|
-
}
|
|
3385
|
-
});
|
|
3616
|
+
return { truncated: false, chunks: 0, duplicateOf: canonical };
|
|
3617
|
+
}
|
|
3386
3618
|
await this.searchService.sync(entityId);
|
|
3387
3619
|
const uniqueDeletedSourceFactIds = Array.from(new Set(deletedSourceFactIds));
|
|
3388
3620
|
for (const factId of uniqueDeletedSourceFactIds) {
|
|
@@ -3398,7 +3630,7 @@ var IngestionService = class {
|
|
|
3398
3630
|
this.searchService.evictCache(entityId);
|
|
3399
3631
|
return { truncated, chunks: chunks.length };
|
|
3400
3632
|
} finally {
|
|
3401
|
-
|
|
3633
|
+
releaseIngestLocks();
|
|
3402
3634
|
}
|
|
3403
3635
|
}
|
|
3404
3636
|
};
|
|
@@ -3522,11 +3754,12 @@ var HEAL_MAX_PROMPT_CHARS = 4e4;
|
|
|
3522
3754
|
var HEAL_BATCH_SIZE = 25;
|
|
3523
3755
|
var HEAL_RECHECK_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
3524
3756
|
var MaintenanceService = class {
|
|
3525
|
-
constructor(db, prefix, options, entryRepo, taskRepo, eventRepo, metadataRepo, searchService, jobManager, embeddingService, promptService, ontologyService) {
|
|
3757
|
+
constructor(db, prefix, options, entryRepo, sourceRefIndexRepo, taskRepo, eventRepo, metadataRepo, searchService, jobManager, embeddingService, promptService, ontologyService) {
|
|
3526
3758
|
this.db = db;
|
|
3527
3759
|
this.prefix = prefix;
|
|
3528
3760
|
this.options = options;
|
|
3529
3761
|
this.entryRepo = entryRepo;
|
|
3762
|
+
this.sourceRefIndexRepo = sourceRefIndexRepo;
|
|
3530
3763
|
this.taskRepo = taskRepo;
|
|
3531
3764
|
this.eventRepo = eventRepo;
|
|
3532
3765
|
this.metadataRepo = metadataRepo;
|
|
@@ -3725,10 +3958,12 @@ var MaintenanceService = class {
|
|
|
3725
3958
|
const entryPromise = params.entryId ? this.entryRepo.softDelete(params.entryId, entityId, tx).then((r) => r.changes > 0) : null;
|
|
3726
3959
|
const taskDeletedPromise = params.taskId ? this.taskRepo.softDeleteById(params.taskId, entityId, tx).then((r) => r.changes > 0) : null;
|
|
3727
3960
|
const refPromise = sourceRef || sourceHash ? this.entryRepo.softDeleteBySource(entityId, tx, sourceRef, sourceHash) : null;
|
|
3961
|
+
const sourceRefIndexPromise = sourceRef ? this.sourceRefIndexRepo.softDeleteByEntityAndSourceRef(entityId, sourceRef, tx).then(() => void 0) : null;
|
|
3728
3962
|
const [entryResult, taskResult, refResult] = await Promise.all([
|
|
3729
3963
|
entryPromise ?? Promise.resolve(false),
|
|
3730
3964
|
taskDeletedPromise ?? Promise.resolve(false),
|
|
3731
|
-
refPromise ?? Promise.resolve(0)
|
|
3965
|
+
refPromise ?? Promise.resolve(0),
|
|
3966
|
+
sourceRefIndexPromise ?? Promise.resolve(void 0)
|
|
3732
3967
|
]);
|
|
3733
3968
|
if (entryResult) deletedEntries++;
|
|
3734
3969
|
if (taskResult) deletedTasks++;
|
|
@@ -5705,6 +5940,7 @@ var WikiMemory = class {
|
|
|
5705
5940
|
}
|
|
5706
5941
|
this.outboxRepo = new OutboxRepository(this.db, this.prefix, !!options.config?.enableOutbox);
|
|
5707
5942
|
this.entryRepo = new EntryRepository(this.db, this.prefix, this.outboxRepo);
|
|
5943
|
+
this.sourceRefIndexRepo = new SourceRefIndexRepository(this.db, this.prefix);
|
|
5708
5944
|
this.taskRepo = new TaskRepository(this.db, this.prefix, this.outboxRepo);
|
|
5709
5945
|
this.eventRepo = new EventRepository(this.db, this.prefix);
|
|
5710
5946
|
this.edgeRepo = new EdgeRepository(this.db, this.prefix);
|
|
@@ -5723,6 +5959,7 @@ var WikiMemory = class {
|
|
|
5723
5959
|
this.prefix,
|
|
5724
5960
|
this.options,
|
|
5725
5961
|
this.entryRepo,
|
|
5962
|
+
this.sourceRefIndexRepo,
|
|
5726
5963
|
this.searchService,
|
|
5727
5964
|
this.jobManager,
|
|
5728
5965
|
this.embeddingService,
|
|
@@ -5734,6 +5971,7 @@ var WikiMemory = class {
|
|
|
5734
5971
|
this.prefix,
|
|
5735
5972
|
this.options,
|
|
5736
5973
|
this.entryRepo,
|
|
5974
|
+
this.sourceRefIndexRepo,
|
|
5737
5975
|
this.taskRepo,
|
|
5738
5976
|
this.eventRepo,
|
|
5739
5977
|
this.metadataRepo,
|
|
@@ -5798,6 +6036,7 @@ var WikiMemory = class {
|
|
|
5798
6036
|
promptService: this.promptService,
|
|
5799
6037
|
graphTraversalService: this.graphTraversalService,
|
|
5800
6038
|
entryRepo: this.entryRepo,
|
|
6039
|
+
sourceRefIndexRepo: this.sourceRefIndexRepo,
|
|
5801
6040
|
metadataRepo: this.metadataRepo,
|
|
5802
6041
|
jobManager: this.jobManager
|
|
5803
6042
|
};
|
|
@@ -5874,7 +6113,7 @@ var WikiMemory = class {
|
|
|
5874
6113
|
const latestHashes = await this.entryRepo.findLatestSourceHashes(entityId, normalized.map((e) => e.sourceRef));
|
|
5875
6114
|
const distinctHashes = Array.from(new Set(normalized.map((e) => e.sourceHash)));
|
|
5876
6115
|
const dupRefs = await Promise.all(
|
|
5877
|
-
distinctHashes.map((h) => this.
|
|
6116
|
+
distinctHashes.map((h) => this.sourceRefIndexRepo.findActiveByEntityAndHash(entityId, h))
|
|
5878
6117
|
);
|
|
5879
6118
|
const dupMap = /* @__PURE__ */ new Map();
|
|
5880
6119
|
for (let i = 0; i < distinctHashes.length; i++) {
|
|
@@ -5883,12 +6122,10 @@ var WikiMemory = class {
|
|
|
5883
6122
|
return normalized.map((e) => {
|
|
5884
6123
|
const stored = latestHashes.get(e.sourceRef);
|
|
5885
6124
|
const changed = stored === void 0 || stored === null || normalizeSourceHash(stored) !== e.sourceHash;
|
|
5886
|
-
const
|
|
5887
|
-
|
|
5888
|
-
if (others.length === 0) {
|
|
6125
|
+
const canonical = dupMap.get(e.sourceHash) ?? null;
|
|
6126
|
+
if (canonical === null || canonical === e.sourceRef) {
|
|
5889
6127
|
return { sourceRef: e.rawSourceRef, changed };
|
|
5890
6128
|
}
|
|
5891
|
-
const canonical = others[0];
|
|
5892
6129
|
return { sourceRef: e.rawSourceRef, changed, duplicateOf: canonical };
|
|
5893
6130
|
});
|
|
5894
6131
|
}
|
|
@@ -5903,13 +6140,17 @@ var WikiMemory = class {
|
|
|
5903
6140
|
return this.entryRepo.listSourceRefs(entityId);
|
|
5904
6141
|
}
|
|
5905
6142
|
/**
|
|
5906
|
-
* Returns the live source_refs for an entity that hold the given source_hash
|
|
5907
|
-
*
|
|
5908
|
-
*
|
|
5909
|
-
*
|
|
6143
|
+
* Returns the live source_refs for an entity that hold the given source_hash.
|
|
6144
|
+
* With v9, source_ref_index is the source of truth for the sourceRef-level
|
|
6145
|
+
* TOCTOU-race invariant: at most one sourceRef can hold a given
|
|
6146
|
+
* (entity_id, source_hash). The result is either a single-element array
|
|
6147
|
+
* (one canonical ref) or empty (no live ref holds the hash). Returned as
|
|
6148
|
+
* an array to preserve the existing public-API shape used by hosts
|
|
6149
|
+
* auditing duplicate-content collisions.
|
|
5910
6150
|
*/
|
|
5911
6151
|
async findSourceRefsByHash(entityId, sourceHash) {
|
|
5912
|
-
|
|
6152
|
+
const canonical = await this.sourceRefIndexRepo.findActiveByEntityAndHash(entityId, sourceHash);
|
|
6153
|
+
return canonical === null ? [] : [canonical];
|
|
5913
6154
|
}
|
|
5914
6155
|
async runPrune(entityId, options) {
|
|
5915
6156
|
return this.maintenanceService.runPrune(entityId, options);
|