@equationalapplications/core-llm-wiki 3.1.0 → 4.0.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.js CHANGED
@@ -16,7 +16,7 @@ async function setupDatabase(db, prefix) {
16
16
  body TEXT NOT NULL,
17
17
  tags TEXT NOT NULL DEFAULT '[]',
18
18
  confidence TEXT NOT NULL DEFAULT 'inferred',
19
- source_type TEXT NOT NULL DEFAULT 'agent_inferred',
19
+ source_type TEXT NOT NULL DEFAULT 'librarian_inferred',
20
20
  source_hash TEXT,
21
21
  source_ref TEXT,
22
22
  created_at INTEGER NOT NULL,
@@ -132,6 +132,18 @@ var WikiBusyError = class extends Error {
132
132
  this.entityId = entityId;
133
133
  }
134
134
  };
135
+ var PrunePartialFailureError = class extends Error {
136
+ constructor(deleted, failedAt, remaining, cause, deletedTasks = 0, deletedEvents = 0) {
137
+ super(`Prune partially failed: deleted ${deleted}, failed at ${failedAt}, ${remaining} remaining`);
138
+ this.name = "PrunePartialFailureError";
139
+ this.deleted = deleted;
140
+ this.failedAt = failedAt;
141
+ this.remaining = remaining;
142
+ this.deletedTasks = deletedTasks;
143
+ this.deletedEvents = deletedEvents;
144
+ this.cause = cause;
145
+ }
146
+ };
135
147
 
136
148
  // src/prompts.ts
137
149
  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.
@@ -198,6 +210,7 @@ function parseEmbedding(blob, text) {
198
210
  }
199
211
 
200
212
  // src/WikiMemory.ts
213
+ var HOOK_TIMEOUT_MARKER = /* @__PURE__ */ Symbol("WikiMemoryHookTimeout");
201
214
  function parseJsonResponse(text) {
202
215
  const firstBrace = text.indexOf("{");
203
216
  const firstBracket = text.indexOf("[");
@@ -580,8 +593,94 @@ var _WikiMemory = class _WikiMemory {
580
593
  _warnCrossEntityCollision(type, id, existingEntityId, targetEntityId) {
581
594
  console.warn(`[WikiMemory] importDump: ${type} id "${id}" already belongs to entity "${existingEntityId}"; skipping for entity "${targetEntityId}"`);
582
595
  }
596
+ /** Maps pre-rename enum strings from older dumps to current source_type values. */
597
+ _normalizeImportedSourceType(raw, ctx) {
598
+ if (raw === "user_document") return "immutable_document";
599
+ if (raw === "agent_inferred") return "librarian_inferred";
600
+ const allowed = ["user_stated", "librarian_inferred", "user_confirmed", "immutable_document"];
601
+ if (allowed.includes(raw)) return raw;
602
+ const where = ctx !== void 0 ? ` for entity "${ctx.entityId}" fact "${ctx.factId}"` : "";
603
+ throw new Error(
604
+ `importDump: invalid source_type "${raw}"${where} (expected one of: ${allowed.join(", ")}, or legacy aliases user_document / agent_inferred)`
605
+ );
606
+ }
607
+ async assertNoLegacySourceTypes() {
608
+ const legacyProbe = await this.db.getFirstAsync(
609
+ `SELECT 1 AS one FROM ${this.prefix}entries
610
+ WHERE source_type IN ('user_document', 'agent_inferred')
611
+ LIMIT 1`,
612
+ []
613
+ );
614
+ if (!legacyProbe) return;
615
+ const legacyCount = await this.db.getFirstAsync(
616
+ `SELECT COUNT(*) as count FROM ${this.prefix}entries
617
+ WHERE source_type IN ('user_document', 'agent_inferred')`,
618
+ []
619
+ );
620
+ const count = legacyCount?.count ?? 0;
621
+ const migrationSQL = `
622
+ -- Migrate legacy source_type values (targets your WikiMemory prefix: ${this.prefix})
623
+ UPDATE ${this.prefix}entries SET source_type = 'immutable_document' WHERE source_type = 'user_document';
624
+ UPDATE ${this.prefix}entries SET source_type = 'librarian_inferred' WHERE source_type = 'agent_inferred';
625
+ `.trim();
626
+ throw new Error(
627
+ `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.
628
+
629
+ ${migrationSQL}
630
+
631
+ After running the migration SQL, restart your application.`
632
+ );
633
+ }
583
634
  async _notifyEmbeddingPersisted(entityId, factId, vector) {
584
- await this.options.vectorRanker?.onEmbeddingPersisted?.({ entityId, factId, vector });
635
+ if (!this.options.vectorRanker?.onEmbeddingPersisted) return;
636
+ const vectorCopy = vector ? vector.slice() : null;
637
+ await this.options.vectorRanker.onEmbeddingPersisted({
638
+ entityId,
639
+ factId,
640
+ vector: vectorCopy
641
+ });
642
+ }
643
+ /**
644
+ * GDPR-critical variant: awaits the hook with a timeout and rethrows failures.
645
+ * Use ONLY on deletion paths. forget() calls after soft-delete UPDATE; runPrune()
646
+ * calls before hard DELETE. For best-effort sync, use _notifyEmbeddingPersisted.
647
+ */
648
+ async _notifyEmbeddingPersistedOrThrow(entityId, factId, vector) {
649
+ if (!this.options.vectorRanker?.onEmbeddingPersisted) return;
650
+ if (this.options.forceDeleteIgnoreRankerHook === true) return;
651
+ const vectorCopy = vector ? vector.slice() : null;
652
+ const rawTimeout = this.options.deletionHookTimeoutMs ?? 3e4;
653
+ if (typeof rawTimeout !== "number" || !Number.isFinite(rawTimeout) || rawTimeout <= 0) {
654
+ throw new Error("Invalid deletionHookTimeoutMs: must be a positive finite number");
655
+ }
656
+ const timeoutMs = rawTimeout;
657
+ let timeoutHandle;
658
+ const timeoutPromise = new Promise((_, reject) => {
659
+ timeoutHandle = setTimeout(
660
+ () => {
661
+ const timeoutError = new Error(`onEmbeddingPersisted timed out after ${timeoutMs}ms`);
662
+ timeoutError[HOOK_TIMEOUT_MARKER] = true;
663
+ reject(timeoutError);
664
+ },
665
+ timeoutMs
666
+ );
667
+ });
668
+ const hookPromise = Promise.resolve(
669
+ this.options.vectorRanker.onEmbeddingPersisted({
670
+ entityId,
671
+ factId,
672
+ vector: vectorCopy
673
+ })
674
+ );
675
+ try {
676
+ await Promise.race([hookPromise, timeoutPromise]);
677
+ } catch (err) {
678
+ hookPromise.catch(() => {
679
+ });
680
+ throw err;
681
+ } finally {
682
+ if (timeoutHandle) clearTimeout(timeoutHandle);
683
+ }
585
684
  }
586
685
  async setup() {
587
686
  const entriesExistedBeforeSetup = await this.db.getFirstAsync(
@@ -633,6 +732,9 @@ var _WikiMemory = class _WikiMemory {
633
732
  );
634
733
  }
635
734
  }
735
+ if (entriesExistedBeforeSetup) {
736
+ await this.assertNoLegacySourceTypes();
737
+ }
636
738
  const rows = await this.db.getAllAsync(`
637
739
  SELECT rowid, source_ref FROM ${this.prefix}entries
638
740
  WHERE source_ref IS NOT NULL
@@ -765,33 +867,77 @@ var _WikiMemory = class _WikiMemory {
765
867
  let deletedEntries = 0;
766
868
  let deletedTasks = 0;
767
869
  let deletedEvents = 0;
768
- const deletedEntryIds = [];
769
870
  if (retainSoftDeletedFor !== null) {
770
871
  const cutoff = now - retainSoftDeletedFor * 864e5;
771
872
  const entriesToDelete = await this.db.getAllAsync(
772
- `SELECT id FROM ${this.prefix}entries
773
- WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at < ?`,
873
+ `SELECT id, entity_id FROM ${this.prefix}entries
874
+ WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at <= ?`,
774
875
  [entityId, cutoff]
775
876
  );
776
- deletedEntryIds.push(...entriesToDelete.map((e) => e.id));
777
- const entryResult = await this.db.runAsync(
778
- `DELETE FROM ${this.prefix}entries
779
- WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at < ?`,
780
- [entityId, cutoff]
781
- );
782
- deletedEntries = entryResult.changes;
877
+ const succeeded = [];
878
+ let failure = null;
879
+ for (const row of entriesToDelete) {
880
+ try {
881
+ await this._notifyEmbeddingPersistedOrThrow(row.entity_id, row.id, null);
882
+ succeeded.push({ entity_id: row.entity_id, id: row.id });
883
+ } catch (err) {
884
+ failure = { factId: row.id, cause: err };
885
+ break;
886
+ }
887
+ }
888
+ if (succeeded.length > 0) {
889
+ const chunkSize = 500;
890
+ for (let i = 0; i < succeeded.length; i += chunkSize) {
891
+ const chunk = succeeded.slice(i, i + chunkSize);
892
+ const placeholders = chunk.map(() => "?").join(",");
893
+ const entryResult = await this.db.runAsync(
894
+ `DELETE FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at <= ? AND id IN (${placeholders})`,
895
+ [entityId, cutoff, ...chunk.map((r) => r.id)]
896
+ );
897
+ deletedEntries += entryResult.changes;
898
+ }
899
+ }
783
900
  const taskResult = await this.db.runAsync(
784
901
  `DELETE FROM ${this.prefix}tasks
785
- WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at < ?`,
902
+ WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at <= ?`,
786
903
  [entityId, cutoff]
787
904
  );
788
905
  deletedTasks = taskResult.changes;
906
+ if (failure) {
907
+ await this.rebuildMiniSearchIndex(entityId);
908
+ this.vectorCache.delete(entityId);
909
+ const remaining = entriesToDelete.length - succeeded.length - 1;
910
+ const isTimeout = failure.cause?.[HOOK_TIMEOUT_MARKER] === true;
911
+ if (isTimeout) {
912
+ throw new PrunePartialFailureError(
913
+ succeeded.length,
914
+ failure.factId,
915
+ remaining,
916
+ new Error("Deletion hook timed out"),
917
+ deletedTasks,
918
+ 0
919
+ // events not yet deleted at this point
920
+ );
921
+ }
922
+ const errMsg = failure.cause?.message ?? "";
923
+ const isValidationError = errMsg.startsWith("Invalid deletionHookTimeoutMs");
924
+ const sanitizedCause = isValidationError ? failure.cause : this._sanitizeRankerError(failure.cause);
925
+ throw new PrunePartialFailureError(
926
+ succeeded.length,
927
+ failure.factId,
928
+ remaining,
929
+ sanitizedCause,
930
+ deletedTasks,
931
+ 0
932
+ // events not yet deleted at this point
933
+ );
934
+ }
789
935
  }
790
936
  if (retainEventsFor !== null) {
791
937
  const cutoff = now - retainEventsFor * 864e5;
792
938
  const eventResult = await this.db.runAsync(
793
939
  `DELETE FROM ${this.prefix}events
794
- WHERE entity_id = ? AND created_at < ?`,
940
+ WHERE entity_id = ? AND created_at <= ?`,
795
941
  [entityId, cutoff]
796
942
  );
797
943
  deletedEvents = eventResult.changes;
@@ -802,14 +948,6 @@ var _WikiMemory = class _WikiMemory {
802
948
  }
803
949
  await this.rebuildMiniSearchIndex(entityId);
804
950
  this.vectorCache.delete(entityId);
805
- const uniqueDeletedIds = Array.from(new Set(deletedEntryIds));
806
- for (const factId of uniqueDeletedIds) {
807
- try {
808
- await this._notifyEmbeddingPersisted(entityId, factId, null);
809
- } catch (hookErr) {
810
- console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during prune for ${factId}:`, hookErr);
811
- }
812
- }
813
951
  return { entries: deletedEntries, tasks: deletedTasks, events: deletedEvents };
814
952
  } finally {
815
953
  this.activeMaintenanceJobs.delete(pruneKey);
@@ -1063,7 +1201,10 @@ var _WikiMemory = class _WikiMemory {
1063
1201
  } catch (rankerErr) {
1064
1202
  const rankerError = rankerErr instanceof Error ? rankerErr : new Error(String(rankerErr));
1065
1203
  const policy = this.options.vectorRankerFallback ?? "js-cosine";
1066
- this.options.onVectorRankerFallback?.({ error: rankerError, policy });
1204
+ this.options.onVectorRankerFallback?.({
1205
+ error: this._sanitizeRankerError(rankerError),
1206
+ policy
1207
+ });
1067
1208
  if (policy === "throw") {
1068
1209
  rankerShouldRethrow = true;
1069
1210
  throw rankerError;
@@ -1127,8 +1268,9 @@ var _WikiMemory = class _WikiMemory {
1127
1268
  scored = [];
1128
1269
  }
1129
1270
  if (this.options.propagateRankerFailureToRetrievalFallback) {
1130
- const mirrored = new Error("Vector ranker failed, falling back");
1131
- mirrored.cause = rankerError;
1271
+ const mirrored = new Error("Vector ranker failed, falling back", {
1272
+ cause: this._sanitizeRankerError(rankerErr)
1273
+ });
1132
1274
  pendingRankerFallbackError = mirrored;
1133
1275
  }
1134
1276
  }
@@ -1288,12 +1430,31 @@ var _WikiMemory = class _WikiMemory {
1288
1430
  if (updatedAtDiff !== 0) return updatedAtDiff;
1289
1431
  return a.id.localeCompare(b.id);
1290
1432
  }
1433
+ /**
1434
+ * Strip potentially sensitive data from ranker errors before exposing to host callbacks.
1435
+ * Preserves error type for debugging but removes message/stack that may contain credentials.
1436
+ * Recursively sanitizes one level of .cause; deeper chains collapse to type only.
1437
+ */
1438
+ _sanitizeRankerError(err) {
1439
+ if (this.options.sanitizeRankerErrors === false) {
1440
+ return err instanceof Error ? err : new Error(String(err));
1441
+ }
1442
+ const typeName = err instanceof Error ? err.constructor?.name ?? "Error" : typeof err;
1443
+ const innerCause = err instanceof Error && err.cause !== void 0 ? new Error(`Caused by: ${err.cause?.constructor?.name ?? typeof err.cause}`) : void 0;
1444
+ const sanitized = new Error(
1445
+ `VectorRanker ${typeName} (message scrubbed for security)`,
1446
+ innerCause ? { cause: innerCause } : void 0
1447
+ );
1448
+ sanitized.name = typeName;
1449
+ return sanitized;
1450
+ }
1291
1451
  /**
1292
1452
  * Score candidate rows using in-process JS cosine similarity.
1293
1453
  * Applies hybrid blending (if weight set) and tie-break sorting before returning.
1294
1454
  */
1295
1455
  async _rankWithJsCosine(args) {
1296
- const { entityId, queryVec, candidateRows, weight, miniSearchScores, populateCache, limit } = args;
1456
+ const queryVec = args.queryVec instanceof Float32Array ? args.queryVec.slice() : Array.from(args.queryVec);
1457
+ const { entityId, candidateRows, weight, miniSearchScores, populateCache, limit } = args;
1297
1458
  let entityCache = this.vectorCache.get(entityId);
1298
1459
  const tooLarge = populateCache && candidateRows.length > _WikiMemory.MAX_VECTOR_CACHE_FACTS_PER_ENTITY;
1299
1460
  if (tooLarge && entityCache) {
@@ -1344,14 +1505,15 @@ var _WikiMemory = class _WikiMemory {
1344
1505
  * Returns scored results ready for hybrid blending and tie-break sorting.
1345
1506
  */
1346
1507
  async _rankWithVectorRanker(args) {
1347
- const { entityId, queryVec, candidateIds, weight, miniSearchScores, limit } = args;
1508
+ const { entityId, candidateIds, weight, miniSearchScores, limit } = args;
1348
1509
  const ranker = this.options.vectorRanker;
1349
1510
  if (!ranker) {
1350
1511
  throw new Error("vectorRanker not configured");
1351
1512
  }
1513
+ const queryVecCopy = args.queryVec instanceof Float32Array ? args.queryVec.slice() : Array.from(args.queryVec);
1352
1514
  const rankerResults = await ranker.rankBySimilarity({
1353
1515
  entityId,
1354
- queryVec,
1516
+ queryVec: queryVecCopy,
1355
1517
  candidateIds,
1356
1518
  limit
1357
1519
  });
@@ -1476,7 +1638,7 @@ ${JSON.stringify(currentFacts, null, 2)}`;
1476
1638
  let skip = false;
1477
1639
  if (newTokens.size >= MIN_TOKENS_TO_QUALIFY) {
1478
1640
  for (const existing of currentFactsRows) {
1479
- if (existing.source_type !== "agent_inferred") continue;
1641
+ if (existing.source_type !== "librarian_inferred") continue;
1480
1642
  const existingTokens = titleTokens(existing.title);
1481
1643
  if (existingTokens.size >= MIN_TOKENS_TO_QUALIFY) {
1482
1644
  if (jaccardScore(newTokens, existingTokens) >= FUZZY_THRESHOLD) {
@@ -1491,7 +1653,7 @@ ${JSON.stringify(currentFacts, null, 2)}`;
1491
1653
  await this.db.runAsync(`
1492
1654
  INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, created_at, updated_at)
1493
1655
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1494
- `, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "agent_inferred", now, now]);
1656
+ `, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "librarian_inferred", now, now]);
1495
1657
  insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
1496
1658
  }
1497
1659
  for (const task of validTasks) {
@@ -1524,25 +1686,25 @@ ${JSON.stringify(currentFacts, null, 2)}`;
1524
1686
  if (orphanAfterDays !== null) {
1525
1687
  const orphanThreshold = now - orphanAfterDays * MS_PER_DAY;
1526
1688
  await this.db.runAsync(`
1527
- UPDATE ${this.prefix}entries
1528
- SET deleted_at = ?, updated_at = ?
1529
- WHERE entity_id = ? AND access_count = 0 AND created_at < ? AND source_type != 'user_document' AND deleted_at IS NULL
1689
+ UPDATE ${this.prefix}entries
1690
+ SET deleted_at = ?, updated_at = ?
1691
+ WHERE entity_id = ? AND access_count = 0 AND created_at <= ? AND source_type != 'immutable_document' AND deleted_at IS NULL
1530
1692
  `, [now, now, entityId, orphanThreshold]);
1531
1693
  }
1532
1694
  if (staleInferredAfterDays !== null) {
1533
1695
  const staleThreshold = now - staleInferredAfterDays * MS_PER_DAY;
1534
1696
  await this.db.runAsync(`
1535
- UPDATE ${this.prefix}entries
1536
- SET confidence = 'tentative', updated_at = ?
1537
- WHERE entity_id = ? AND confidence = 'inferred' AND (last_accessed_at < ? OR (last_accessed_at IS NULL AND created_at < ?)) AND source_type != 'user_document' AND deleted_at IS NULL
1697
+ UPDATE ${this.prefix}entries
1698
+ SET confidence = 'tentative', updated_at = ?
1699
+ WHERE entity_id = ? AND confidence = 'inferred' AND (last_accessed_at <= ? OR (last_accessed_at IS NULL AND created_at <= ?)) AND source_type != 'immutable_document' AND deleted_at IS NULL
1538
1700
  `, [now, entityId, staleThreshold, staleThreshold]);
1539
1701
  }
1540
1702
  });
1541
1703
  const allFactsRows = await this.db.getAllAsync(`SELECT * FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`, [entityId]);
1542
1704
  const allTasks = await this.db.getAllAsync(`SELECT * FROM ${this.prefix}tasks WHERE entity_id = ? AND status IN ('pending', 'in_progress') AND deleted_at IS NULL`, [entityId]);
1543
1705
  const recentEvents = await this.db.getAllAsync(`SELECT * FROM ${this.prefix}events WHERE entity_id = ? ORDER BY created_at DESC LIMIT 20`, [entityId]);
1544
- const healCandidates = allFactsRows.filter((f) => f.source_type !== "user_document");
1545
- const documentAnchors = allFactsRows.filter((f) => f.source_type === "user_document").map(({ id, title, source_ref }) => ({ id, title, source_ref }));
1706
+ const healCandidates = allFactsRows.filter((f) => f.source_type !== "immutable_document");
1707
+ const documentAnchors = allFactsRows.filter((f) => f.source_type === "immutable_document").map(({ id, title, source_ref }) => ({ id, title, source_ref }));
1546
1708
  const userPrompt = `Heal Candidates:
1547
1709
  ${JSON.stringify(healCandidates.map((f) => {
1548
1710
  const { embedding: _embedding, embedding_blob: _blob, ...rest } = f;
@@ -1585,7 +1747,7 @@ The following document anchors are provided for contradiction detection only. Do
1585
1747
  await this.db.runAsync(`
1586
1748
  INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, created_at, updated_at)
1587
1749
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1588
- `, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "agent_inferred", now, now]);
1750
+ `, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "librarian_inferred", now, now]);
1589
1751
  insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
1590
1752
  }
1591
1753
  });
@@ -1885,6 +2047,7 @@ The following document anchors are provided for contradiction detection only. Do
1885
2047
  this.activeMaintenanceJobs.add(this._importKey(entityId));
1886
2048
  }
1887
2049
  try {
2050
+ await this.assertNoLegacySourceTypes();
1888
2051
  for (const [entityId, bundle] of Object.entries(dump.entities)) {
1889
2052
  await this._doImportEntity(entityId, bundle, merge);
1890
2053
  }
@@ -1938,6 +2101,10 @@ The following document anchors are provided for contradiction detection only. Do
1938
2101
  }
1939
2102
  }
1940
2103
  for (const fact of bundle.facts) {
2104
+ const sourceType = this._normalizeImportedSourceType(String(fact.source_type), {
2105
+ entityId,
2106
+ factId: fact.id
2107
+ });
1941
2108
  const tagsJson = JSON.stringify(Array.isArray(fact.tags) ? fact.tags : []);
1942
2109
  const safeUpdatedAt = Number.isFinite(fact.updated_at) ? fact.updated_at : 0;
1943
2110
  const existing = existingFactsById.get(fact.id);
@@ -1986,14 +2153,14 @@ The following document anchors are provided for contradiction detection only. Do
1986
2153
  if (blobData != null) {
1987
2154
  await this.db.runAsync(
1988
2155
  `UPDATE ${this.prefix}entries SET entity_id = ?, title = ?, body = ?, tags = ?, confidence = ?, source_type = ?, source_hash = ?, source_ref = ?, created_at = ?, updated_at = ?, last_accessed_at = ?, access_count = ?, deleted_at = ?, embedding_blob = ?, embedding = NULL WHERE id = ?`,
1989
- [entityId, fact.title, fact.body, tagsJson, fact.confidence, fact.source_type, fact.source_hash, fact.source_ref, fact.created_at, safeUpdatedAt, fact.last_accessed_at, fact.access_count, fact.deleted_at, blobData, fact.id]
2156
+ [entityId, fact.title, fact.body, tagsJson, fact.confidence, sourceType, fact.source_hash, fact.source_ref, fact.created_at, safeUpdatedAt, fact.last_accessed_at, fact.access_count, fact.deleted_at, blobData, fact.id]
1990
2157
  );
1991
2158
  factsWithPreservedBlob.set(fact.id, blobData);
1992
2159
  if (!fact.deleted_at) preservedBlobDims.add(blobData.byteLength / 4);
1993
2160
  } else {
1994
2161
  await this.db.runAsync(
1995
2162
  `UPDATE ${this.prefix}entries SET entity_id = ?, title = ?, body = ?, tags = ?, confidence = ?, source_type = ?, source_hash = ?, source_ref = ?, created_at = ?, updated_at = ?, last_accessed_at = ?, access_count = ?, deleted_at = ?, embedding_blob = NULL, embedding = NULL WHERE id = ?`,
1996
- [entityId, fact.title, fact.body, tagsJson, fact.confidence, fact.source_type, fact.source_hash, fact.source_ref, fact.created_at, safeUpdatedAt, fact.last_accessed_at, fact.access_count, fact.deleted_at, fact.id]
2163
+ [entityId, fact.title, fact.body, tagsJson, fact.confidence, sourceType, fact.source_hash, fact.source_ref, fact.created_at, safeUpdatedAt, fact.last_accessed_at, fact.access_count, fact.deleted_at, fact.id]
1997
2164
  );
1998
2165
  }
1999
2166
  existingFactsById.set(fact.id, { id: fact.id, entity_id: entityId, updated_at: safeUpdatedAt });
@@ -2003,14 +2170,14 @@ The following document anchors are provided for contradiction detection only. Do
2003
2170
  if (blobData != null) {
2004
2171
  await this.db.runAsync(
2005
2172
  `INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, source_hash, source_ref, created_at, updated_at, last_accessed_at, access_count, deleted_at, embedding_blob) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2006
- [fact.id, entityId, fact.title, fact.body, tagsJson, fact.confidence, fact.source_type, fact.source_hash, fact.source_ref, fact.created_at, safeUpdatedAt, fact.last_accessed_at, fact.access_count, fact.deleted_at, blobData]
2173
+ [fact.id, entityId, fact.title, fact.body, tagsJson, fact.confidence, sourceType, fact.source_hash, fact.source_ref, fact.created_at, safeUpdatedAt, fact.last_accessed_at, fact.access_count, fact.deleted_at, blobData]
2007
2174
  );
2008
2175
  factsWithPreservedBlob.set(fact.id, blobData);
2009
2176
  if (!fact.deleted_at) preservedBlobDims.add(blobData.byteLength / 4);
2010
2177
  } else {
2011
2178
  await this.db.runAsync(
2012
2179
  `INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, source_hash, source_ref, created_at, updated_at, last_accessed_at, access_count, deleted_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2013
- [fact.id, entityId, fact.title, fact.body, tagsJson, fact.confidence, fact.source_type, fact.source_hash, fact.source_ref, fact.created_at, safeUpdatedAt, fact.last_accessed_at, fact.access_count, fact.deleted_at]
2180
+ [fact.id, entityId, fact.title, fact.body, tagsJson, fact.confidence, sourceType, fact.source_hash, fact.source_ref, fact.created_at, safeUpdatedAt, fact.last_accessed_at, fact.access_count, fact.deleted_at]
2014
2181
  );
2015
2182
  }
2016
2183
  existingFactsById.set(fact.id, { id: fact.id, entity_id: entityId, updated_at: safeUpdatedAt });
@@ -2171,11 +2338,15 @@ The following document anchors are provided for contradiction detection only. Do
2171
2338
  let deletedTasks = 0;
2172
2339
  const deletedEntryIds = [];
2173
2340
  if (params.clearAll) {
2174
- const entriesToDelete = await this.db.getAllAsync(
2341
+ const newDeletions = await this.db.getAllAsync(
2175
2342
  `SELECT id FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`,
2176
2343
  [entityId]
2177
2344
  );
2178
- deletedEntryIds.push(...entriesToDelete.map((e) => e.id));
2345
+ const alreadySoftDeleted = await this.db.getAllAsync(
2346
+ `SELECT id FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NOT NULL`,
2347
+ [entityId]
2348
+ );
2349
+ deletedEntryIds.push(...newDeletions.map((e) => e.id), ...alreadySoftDeleted.map((e) => e.id));
2179
2350
  const [entriesRes, tasksRes] = await Promise.all([
2180
2351
  this.db.runAsync(`UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE entity_id = ? AND deleted_at IS NULL`, [now, now, entityId]),
2181
2352
  this.db.runAsync(`UPDATE ${this.prefix}tasks SET deleted_at = ?, updated_at = ? WHERE entity_id = ? AND deleted_at IS NULL`, [now, now, entityId])
@@ -2195,13 +2366,13 @@ The following document anchors are provided for contradiction detection only. Do
2195
2366
  if (params.sourceHash !== void 0 && !sourceHash) throw new Error("Invalid sourceHash (must be 64-char hex string)");
2196
2367
  if (params.entryId) {
2197
2368
  const entry = await this.db.getFirstAsync(
2198
- `SELECT id FROM ${this.prefix}entries WHERE id = ? AND entity_id = ? AND deleted_at IS NULL`,
2369
+ `SELECT id FROM ${this.prefix}entries WHERE id = ? AND entity_id = ?`,
2199
2370
  [params.entryId, entityId]
2200
2371
  );
2201
2372
  if (entry) deletedEntryIds.push(entry.id);
2202
2373
  }
2203
2374
  if (sourceRef || sourceHash) {
2204
- let q = `SELECT id FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`;
2375
+ let q = `SELECT id FROM ${this.prefix}entries WHERE entity_id = ?`;
2205
2376
  const args = [entityId];
2206
2377
  if (sourceRef) {
2207
2378
  q += ` AND source_ref = ?`;
@@ -2244,9 +2415,26 @@ The following document anchors are provided for contradiction detection only. Do
2244
2415
  const uniqueDeletedIds = Array.from(new Set(deletedEntryIds));
2245
2416
  for (const factId of uniqueDeletedIds) {
2246
2417
  try {
2247
- await this._notifyEmbeddingPersisted(entityId, factId, null);
2418
+ await this._notifyEmbeddingPersistedOrThrow(entityId, factId, null);
2248
2419
  } catch (hookErr) {
2249
- console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during forget for ${factId}:`, hookErr);
2420
+ const isTimeout = hookErr?.[HOOK_TIMEOUT_MARKER] === true;
2421
+ if (isTimeout) {
2422
+ throw new Error(
2423
+ `forget(${entityId}/${factId}) failed: ${hookErr.message}`
2424
+ );
2425
+ }
2426
+ const errMsg = hookErr?.message ?? "";
2427
+ const isValidationError = errMsg.startsWith("Invalid deletionHookTimeoutMs");
2428
+ if (isValidationError) {
2429
+ throw new Error(
2430
+ `forget(${entityId}/${factId}) failed: ${errMsg}`,
2431
+ { cause: hookErr }
2432
+ );
2433
+ }
2434
+ throw new Error(
2435
+ `forget(${entityId}/${factId}) failed: ANN cleanup hook rejected`,
2436
+ { cause: this._sanitizeRankerError(hookErr) }
2437
+ );
2250
2438
  }
2251
2439
  }
2252
2440
  return { deleted: { entries: deletedEntries, tasks: deletedTasks } };
@@ -2336,7 +2524,7 @@ ${chunk}`;
2336
2524
  await this.db.runAsync(
2337
2525
  `INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, source_hash, source_ref, created_at, updated_at)
2338
2526
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2339
- [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "user_document", sourceHash, sourceRef, now, now]
2527
+ [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "immutable_document", sourceHash, sourceRef, now, now]
2340
2528
  );
2341
2529
  insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
2342
2530
  }
@@ -2595,10 +2783,12 @@ function createWiki(db, options) {
2595
2783
  return new WikiMemory(db, options);
2596
2784
  }
2597
2785
 
2786
+ exports.PrunePartialFailureError = PrunePartialFailureError;
2598
2787
  exports.WikiBusyError = WikiBusyError;
2599
2788
  exports.WikiMemory = WikiMemory;
2600
2789
  exports.createWiki = createWiki;
2601
2790
  exports.formatContext = formatContext;
2602
2791
  exports.formatMemoryDump = formatMemoryDump;
2792
+ exports.parseEmbedding = parseEmbedding;
2603
2793
  //# sourceMappingURL=index.js.map
2604
2794
  //# sourceMappingURL=index.js.map