@equationalapplications/core-llm-wiki 4.19.0 → 4.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { __privateAdd, MetadataRepository, EmbeddingService, SearchService, JobManager, PromptService, IngestionService, MaintenanceService, ImportExportService, RetrievalService, WriteService, __privateGet, __privateSet, normalizeSourceRef, normalizeSourceHash, entitySummaryMetaKey, generateId, BaseRepository, emptyManifest, resolveNodeType, validateInlineEdges, resolveEdgeDefinition, normalizeTitleKey } from './chunk-3GMXRLKU.mjs';
2
- export { HOOK_TIMEOUT_MARKER, PromptService, PrunePartialFailureError, WikiBusyError, configureRandomSource, parseEmbedding } from './chunk-3GMXRLKU.mjs';
1
+ import { __privateAdd, MetadataRepository, EmbeddingService, SearchService, JobManager, PromptService, IngestionService, MaintenanceService, ImportExportService, RetrievalService, WriteService, __privateGet, __privateSet, normalizeSourceRef, normalizeSourceHash, entitySummaryMetaKey, generateId, BaseRepository, emptyManifest, resolveNodeType, validateInlineEdges, resolveEdgeDefinition, normalizeTitleKey, WikiTransactionError, extractSqliteCode } from './chunk-UYLYN5N4.mjs';
2
+ export { HOOK_TIMEOUT_MARKER, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, WikiBusyError, WikiTransactionError, configureRandomSource, parseEmbedding } from './chunk-UYLYN5N4.mjs';
3
3
  import { appendRelatedSection, buildConceptDocument, buildLogMd, buildEntityIndexMd, buildRootIndexMd, isAllowedOkfPath, parseRootIndexMd, parseEntityIndexMd, parseConcept, splitRelatedSection, extractMarkdownLinks, parseLogMd, parseEventIdComment, appendEventIdComment } from '@equationalapplications/core-okf';
4
4
 
5
5
  // src/db/schema.ts
@@ -22,7 +22,8 @@ async function setupDatabase(db, prefix) {
22
22
  deleted_at INTEGER,
23
23
  embedding TEXT,
24
24
  embedding_blob BLOB,
25
- okf_type TEXT
25
+ okf_type TEXT,
26
+ ontology_checked_at INTEGER
26
27
  );
27
28
 
28
29
  CREATE INDEX IF NOT EXISTS ${prefix}entries_entity_idx ON ${prefix}entries(entity_id);
@@ -104,6 +105,50 @@ async function setupDatabase(db, prefix) {
104
105
  `);
105
106
  }
106
107
 
108
+ // src/db/serializedAdapter.ts
109
+ function isDriverError(err) {
110
+ return extractSqliteCode(err) !== void 0;
111
+ }
112
+ function withOverrides(target, overrides) {
113
+ return new Proxy(target, {
114
+ get(obj, prop, receiver) {
115
+ if (prop in overrides) return Reflect.get(overrides, prop, receiver);
116
+ const value = Reflect.get(obj, prop, obj);
117
+ return typeof value === "function" ? value.bind(obj) : value;
118
+ }
119
+ });
120
+ }
121
+ function guardReentrancy(tx) {
122
+ return withOverrides(tx, {
123
+ withTransactionAsync() {
124
+ throw new Error(
125
+ "Nested withTransactionAsync is not supported: you are already inside a transaction. Pass the current `tx` down instead of opening a new transaction."
126
+ );
127
+ }
128
+ });
129
+ }
130
+ var DEADLOCK_WARN_MS = 1e4;
131
+ function withSerializedTransactions(db) {
132
+ let queue = Promise.resolve();
133
+ return withOverrides(db, {
134
+ withTransactionAsync(fn) {
135
+ const warn = setTimeout(() => {
136
+ console.warn(
137
+ "[core-llm-wiki] Transaction queued >10s \u2014 possible deadlock. Inside a transaction callback, use the `tx` parameter, never the outer database handle."
138
+ );
139
+ }, DEADLOCK_WARN_MS);
140
+ const run = queue.catch(() => void 0).then(() => {
141
+ clearTimeout(warn);
142
+ return db.withTransactionAsync((tx) => fn(guardReentrancy(tx ?? db)));
143
+ }).catch((e) => {
144
+ throw isDriverError(e) ? new WikiTransactionError("Transaction failed", { cause: e }) : e;
145
+ });
146
+ queue = run;
147
+ return run;
148
+ }
149
+ });
150
+ }
151
+
107
152
  // src/db/migrations.ts
108
153
  var MIGRATIONS = [
109
154
  {
@@ -204,6 +249,20 @@ var MIGRATIONS = [
204
249
  );
205
250
  `);
206
251
  }
252
+ },
253
+ {
254
+ version: 7,
255
+ description: "Add ontology_checked_at to entries for ontology backfill recheck cooldown",
256
+ run: async (db, prefix) => {
257
+ const cols = await db.getAllAsync(
258
+ `PRAGMA table_info(${prefix}entries)`
259
+ );
260
+ if (!cols.some((c) => c.name === "ontology_checked_at")) {
261
+ await db.execAsync(
262
+ `ALTER TABLE ${prefix}entries ADD COLUMN ontology_checked_at INTEGER`
263
+ );
264
+ }
265
+ }
207
266
  }
208
267
  ];
209
268
  for (let i = 1; i < MIGRATIONS.length; i++) {
@@ -952,6 +1011,79 @@ var EntryRepository = class extends BaseRepository {
952
1011
  );
953
1012
  return rows.map(mapRowToFact);
954
1013
  }
1014
+ /**
1015
+ * Live untyped facts eligible for ontology backfill, oldest first.
1016
+ * Skips facts checked within the recheck cooldown (ontology_checked_at > recheckCutoff).
1017
+ */
1018
+ async findUntypedByEntityId(entityId, limit, recheckCutoff, tx) {
1019
+ const executor = this.getExecutor(tx);
1020
+ const rows = await executor.getAllAsync(
1021
+ `SELECT * FROM ${this.prefix}entries
1022
+ WHERE entity_id = ? AND okf_type IS NULL AND deleted_at IS NULL
1023
+ AND (ontology_checked_at IS NULL OR ontology_checked_at <= ?)
1024
+ ORDER BY updated_at ASC LIMIT ?`,
1025
+ [entityId, recheckCutoff, limit]
1026
+ );
1027
+ return rows.map(mapRowToFact);
1028
+ }
1029
+ /** Counts live untyped facts: eligible (past cooldown) vs deferred (in cooldown). */
1030
+ async countUntypedByEntityId(entityId, recheckCutoff, tx) {
1031
+ const executor = this.getExecutor(tx);
1032
+ const row = await executor.getFirstAsync(
1033
+ `SELECT
1034
+ SUM(CASE WHEN ontology_checked_at IS NULL OR ontology_checked_at <= ? THEN 1 ELSE 0 END) AS eligible,
1035
+ SUM(CASE WHEN ontology_checked_at IS NOT NULL AND ontology_checked_at > ? THEN 1 ELSE 0 END) AS deferred
1036
+ FROM ${this.prefix}entries
1037
+ WHERE entity_id = ? AND okf_type IS NULL AND deleted_at IS NULL`,
1038
+ [recheckCutoff, recheckCutoff, entityId]
1039
+ );
1040
+ return { eligible: Number(row?.eligible ?? 0), deferred: Number(row?.deferred ?? 0) };
1041
+ }
1042
+ /**
1043
+ * Sets okf_type only when currently NULL — additive by construction: a
1044
+ * concurrently-typed fact is never overwritten. Returns changes for the caller
1045
+ * to distinguish applied vs skipped.
1046
+ */
1047
+ async updateOkfType(id, entityId, okfType, tx) {
1048
+ const result = await tx.runAsync(
1049
+ `UPDATE ${this.prefix}entries SET okf_type = ?
1050
+ WHERE id = ? AND entity_id = ? AND okf_type IS NULL AND deleted_at IS NULL`,
1051
+ [okfType, id, entityId]
1052
+ );
1053
+ return { changes: result.changes };
1054
+ }
1055
+ /**
1056
+ * Stamps the backfill recheck cooldown. NEVER touches updated_at — import
1057
+ * merge resolution is last-write-wins on updated_at and a bump here would
1058
+ * make an unchanged local fact beat a genuinely newer remote edit.
1059
+ */
1060
+ async markOntologyChecked(ids, entityId, now, tx) {
1061
+ if (ids.length === 0) return;
1062
+ for (let i = 0; i < ids.length; i += this.chunkSize) {
1063
+ const chunk = ids.slice(i, i + this.chunkSize);
1064
+ const placeholders = chunk.map(() => "?").join(",");
1065
+ await tx.runAsync(
1066
+ `UPDATE ${this.prefix}entries SET ontology_checked_at = ?
1067
+ WHERE id IN (${placeholders}) AND entity_id = ? AND deleted_at IS NULL`,
1068
+ [now, ...chunk, entityId]
1069
+ );
1070
+ }
1071
+ }
1072
+ /**
1073
+ * Lightweight full-breadth title index (id, title, okf_type) over all live
1074
+ * typed facts. Used by ontology backfill edge resolution — a recent-N window
1075
+ * would miss an old fact's contemporaries. Untyped rows are excluded: they
1076
+ * can never pass the edge target-type check, and batch facts typed during
1077
+ * the run are re-added to the in-memory index by the caller.
1078
+ */
1079
+ async findTitleIndexByEntityId(entityId, tx) {
1080
+ const executor = this.getExecutor(tx);
1081
+ const rows = await executor.getAllAsync(
1082
+ `SELECT id, title, okf_type FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL AND okf_type IS NOT NULL`,
1083
+ [entityId]
1084
+ );
1085
+ return rows.map((r) => ({ id: r.id, title: r.title, okf_type: r.okf_type ?? null }));
1086
+ }
955
1087
  };
956
1088
 
957
1089
  // src/repositories/OutboxRepository.ts
@@ -1372,6 +1504,7 @@ var CONFIDENCE_RANK = {
1372
1504
  var EdgeRepository = class extends BaseRepository {
1373
1505
  /**
1374
1506
  * Insert an edge, silently skipping on primary-key or uniqueness conflicts.
1507
+ * Returns true when a row was inserted, false when skipped as a duplicate.
1375
1508
  * Throws when the insert was skipped due to an id collision with a different edge tuple.
1376
1509
  */
1377
1510
  async addIgnoreDuplicate(edge, tx) {
@@ -1381,17 +1514,18 @@ var EdgeRepository = class extends BaseRepository {
1381
1514
  VALUES (?, ?, ?, ?, ?, ?)`,
1382
1515
  [edge.id, edge.entity_id, edge.source_id, edge.target_id, edge.edge_type, edge.created_at]
1383
1516
  );
1384
- if (result.changes > 0) return;
1517
+ if (result.changes > 0) return true;
1385
1518
  const existing = await executor.getFirstAsync(
1386
1519
  `SELECT entity_id, source_id, target_id, edge_type FROM ${this.prefix}edges WHERE id = ?`,
1387
1520
  [edge.id]
1388
1521
  );
1389
- if (!existing) return;
1522
+ if (!existing) return false;
1390
1523
  if (String(existing.entity_id) !== edge.entity_id || String(existing.source_id) !== edge.source_id || String(existing.target_id) !== edge.target_id || String(existing.edge_type) !== edge.edge_type) {
1391
1524
  throw new Error(
1392
1525
  `Edge id collision: ${JSON.stringify(edge.id)} already exists with a different (entity_id, source_id, target_id, edge_type) tuple`
1393
1526
  );
1394
1527
  }
1528
+ return false;
1395
1529
  }
1396
1530
  async getByEntityId(entityId, tx) {
1397
1531
  const executor = this.getExecutor(tx);
@@ -1591,7 +1725,8 @@ var OntologyService = class {
1591
1725
  return { okf_type: canonical, edges };
1592
1726
  }
1593
1727
  async resolveAndPersistEdges(entityId, sourceId, sourceType, edges, manifest, titleIndex, tx, now) {
1594
- if (!sourceType || edges.length === 0) return;
1728
+ if (!sourceType || edges.length === 0) return 0;
1729
+ let persisted = 0;
1595
1730
  for (const edge of edges) {
1596
1731
  const def = resolveEdgeDefinition(edge.edge_type, manifest);
1597
1732
  if (!def || def.source_type.toLowerCase() !== sourceType.toLowerCase()) continue;
@@ -1607,8 +1742,10 @@ var OntologyService = class {
1607
1742
  edge_type: def.type,
1608
1743
  created_at: now
1609
1744
  };
1610
- await this.edgeRepo.addIgnoreDuplicate(wikiEdge, tx);
1745
+ const inserted = await this.edgeRepo.addIgnoreDuplicate(wikiEdge, tx);
1746
+ if (inserted) persisted++;
1611
1747
  }
1748
+ return persisted;
1612
1749
  }
1613
1750
  };
1614
1751
 
@@ -1651,7 +1788,7 @@ var WikiMemory = class {
1651
1788
  constructor(db, options) {
1652
1789
  /** Emits `__testAccess` console warning at most once per instance when NODE_ENV ≠ "test". */
1653
1790
  __privateAdd(this, _testAccessNonTestEnvWarned, false);
1654
- this.db = db;
1791
+ this.db = withSerializedTransactions(db);
1655
1792
  this.options = options;
1656
1793
  this.prefix = options.config?.tablePrefix ?? "llm_wiki_";
1657
1794
  if (!TABLE_PREFIX_PATTERN.test(this.prefix)) {
@@ -1659,12 +1796,12 @@ var WikiMemory = class {
1659
1796
  `Invalid tablePrefix: ${JSON.stringify(this.prefix)}. Must match ${TABLE_PREFIX_PATTERN} (letter, then alphanumeric/underscore, ending in "_", max 32 chars total).`
1660
1797
  );
1661
1798
  }
1662
- this.outboxRepo = new OutboxRepository(db, this.prefix, !!options.config?.enableOutbox);
1663
- this.entryRepo = new EntryRepository(db, this.prefix, this.outboxRepo);
1664
- this.taskRepo = new TaskRepository(db, this.prefix, this.outboxRepo);
1665
- this.eventRepo = new EventRepository(db, this.prefix);
1666
- this.edgeRepo = new EdgeRepository(db, this.prefix);
1667
- this.metadataRepo = new MetadataRepository(db, this.prefix);
1799
+ this.outboxRepo = new OutboxRepository(this.db, this.prefix, !!options.config?.enableOutbox);
1800
+ this.entryRepo = new EntryRepository(this.db, this.prefix, this.outboxRepo);
1801
+ this.taskRepo = new TaskRepository(this.db, this.prefix, this.outboxRepo);
1802
+ this.eventRepo = new EventRepository(this.db, this.prefix);
1803
+ this.edgeRepo = new EdgeRepository(this.db, this.prefix);
1804
+ this.metadataRepo = new MetadataRepository(this.db, this.prefix);
1668
1805
  this.ontologyService = new OntologyService(
1669
1806
  this.metadataRepo,
1670
1807
  this.edgeRepo,
@@ -1848,6 +1985,24 @@ var WikiMemory = class {
1848
1985
  async runHeal(entityId, options) {
1849
1986
  return this.maintenanceService.runHeal(entityId, options);
1850
1987
  }
1988
+ /**
1989
+ * Types already-persisted untyped facts (okf_type IS NULL) in place via one
1990
+ * librarian-style LLM call. Strictly additive: never creates, deletes, or
1991
+ * rewrites facts; never overwrites an existing okf_type. Free when there is
1992
+ * nothing to do (ontology off, or zero eligible untyped facts).
1993
+ *
1994
+ * Hosts own the trigger cadence (e.g. after each sync); loop `while
1995
+ * result.remaining > 0` for convergence. Throws WikiBusyError when another
1996
+ * backfill (or conflicting maintenance op) is running for the entity.
1997
+ *
1998
+ * @param options.promptOverride - Applies only to this call. For persistent
1999
+ * customization set `options.config.prompts.ontologyBackfillSystemPrompt`.
2000
+ * @param options.batchSize - Facts per run (default 25) for providers with
2001
+ * tighter context limits.
2002
+ */
2003
+ async runOntologyBackfill(entityId, options) {
2004
+ return this.maintenanceService.runOntologyBackfill(entityId, options);
2005
+ }
1851
2006
  async runReembed(entityId, opts) {
1852
2007
  return this.maintenanceService.runReembed(entityId, opts);
1853
2008
  }