@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.mjs CHANGED
@@ -10,7 +10,7 @@ async function setupDatabase(db, prefix) {
10
10
  body TEXT NOT NULL,
11
11
  tags TEXT NOT NULL DEFAULT '[]',
12
12
  confidence TEXT NOT NULL DEFAULT 'inferred',
13
- source_type TEXT NOT NULL DEFAULT 'agent_inferred',
13
+ source_type TEXT NOT NULL DEFAULT 'librarian_inferred',
14
14
  source_hash TEXT,
15
15
  source_ref TEXT,
16
16
  created_at INTEGER NOT NULL,
@@ -126,6 +126,18 @@ var WikiBusyError = class extends Error {
126
126
  this.entityId = entityId;
127
127
  }
128
128
  };
129
+ var PrunePartialFailureError = class extends Error {
130
+ constructor(deleted, failedAt, remaining, cause, deletedTasks = 0, deletedEvents = 0) {
131
+ super(`Prune partially failed: deleted ${deleted}, failed at ${failedAt}, ${remaining} remaining`);
132
+ this.name = "PrunePartialFailureError";
133
+ this.deleted = deleted;
134
+ this.failedAt = failedAt;
135
+ this.remaining = remaining;
136
+ this.deletedTasks = deletedTasks;
137
+ this.deletedEvents = deletedEvents;
138
+ this.cause = cause;
139
+ }
140
+ };
129
141
 
130
142
  // src/prompts.ts
131
143
  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.
@@ -192,6 +204,7 @@ function parseEmbedding(blob, text) {
192
204
  }
193
205
 
194
206
  // src/WikiMemory.ts
207
+ var HOOK_TIMEOUT_MARKER = /* @__PURE__ */ Symbol("WikiMemoryHookTimeout");
195
208
  function parseJsonResponse(text) {
196
209
  const firstBrace = text.indexOf("{");
197
210
  const firstBracket = text.indexOf("[");
@@ -574,8 +587,94 @@ var _WikiMemory = class _WikiMemory {
574
587
  _warnCrossEntityCollision(type, id, existingEntityId, targetEntityId) {
575
588
  console.warn(`[WikiMemory] importDump: ${type} id "${id}" already belongs to entity "${existingEntityId}"; skipping for entity "${targetEntityId}"`);
576
589
  }
590
+ /** Maps pre-rename enum strings from older dumps to current source_type values. */
591
+ _normalizeImportedSourceType(raw, ctx) {
592
+ if (raw === "user_document") return "immutable_document";
593
+ if (raw === "agent_inferred") return "librarian_inferred";
594
+ const allowed = ["user_stated", "librarian_inferred", "user_confirmed", "immutable_document"];
595
+ if (allowed.includes(raw)) return raw;
596
+ const where = ctx !== void 0 ? ` for entity "${ctx.entityId}" fact "${ctx.factId}"` : "";
597
+ throw new Error(
598
+ `importDump: invalid source_type "${raw}"${where} (expected one of: ${allowed.join(", ")}, or legacy aliases user_document / agent_inferred)`
599
+ );
600
+ }
601
+ async assertNoLegacySourceTypes() {
602
+ const legacyProbe = await this.db.getFirstAsync(
603
+ `SELECT 1 AS one FROM ${this.prefix}entries
604
+ WHERE source_type IN ('user_document', 'agent_inferred')
605
+ LIMIT 1`,
606
+ []
607
+ );
608
+ if (!legacyProbe) return;
609
+ const legacyCount = await this.db.getFirstAsync(
610
+ `SELECT COUNT(*) as count FROM ${this.prefix}entries
611
+ WHERE source_type IN ('user_document', 'agent_inferred')`,
612
+ []
613
+ );
614
+ const count = legacyCount?.count ?? 0;
615
+ const migrationSQL = `
616
+ -- Migrate legacy source_type values (targets your WikiMemory prefix: ${this.prefix})
617
+ UPDATE ${this.prefix}entries SET source_type = 'immutable_document' WHERE source_type = 'user_document';
618
+ UPDATE ${this.prefix}entries SET source_type = 'librarian_inferred' WHERE source_type = 'agent_inferred';
619
+ `.trim();
620
+ throw new Error(
621
+ `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.
622
+
623
+ ${migrationSQL}
624
+
625
+ After running the migration SQL, restart your application.`
626
+ );
627
+ }
577
628
  async _notifyEmbeddingPersisted(entityId, factId, vector) {
578
- await this.options.vectorRanker?.onEmbeddingPersisted?.({ entityId, factId, vector });
629
+ if (!this.options.vectorRanker?.onEmbeddingPersisted) return;
630
+ const vectorCopy = vector ? vector.slice() : null;
631
+ await this.options.vectorRanker.onEmbeddingPersisted({
632
+ entityId,
633
+ factId,
634
+ vector: vectorCopy
635
+ });
636
+ }
637
+ /**
638
+ * GDPR-critical variant: awaits the hook with a timeout and rethrows failures.
639
+ * Use ONLY on deletion paths. forget() calls after soft-delete UPDATE; runPrune()
640
+ * calls before hard DELETE. For best-effort sync, use _notifyEmbeddingPersisted.
641
+ */
642
+ async _notifyEmbeddingPersistedOrThrow(entityId, factId, vector) {
643
+ if (!this.options.vectorRanker?.onEmbeddingPersisted) return;
644
+ if (this.options.forceDeleteIgnoreRankerHook === true) return;
645
+ const vectorCopy = vector ? vector.slice() : null;
646
+ const rawTimeout = this.options.deletionHookTimeoutMs ?? 3e4;
647
+ if (typeof rawTimeout !== "number" || !Number.isFinite(rawTimeout) || rawTimeout <= 0) {
648
+ throw new Error("Invalid deletionHookTimeoutMs: must be a positive finite number");
649
+ }
650
+ const timeoutMs = rawTimeout;
651
+ let timeoutHandle;
652
+ const timeoutPromise = new Promise((_, reject) => {
653
+ timeoutHandle = setTimeout(
654
+ () => {
655
+ const timeoutError = new Error(`onEmbeddingPersisted timed out after ${timeoutMs}ms`);
656
+ timeoutError[HOOK_TIMEOUT_MARKER] = true;
657
+ reject(timeoutError);
658
+ },
659
+ timeoutMs
660
+ );
661
+ });
662
+ const hookPromise = Promise.resolve(
663
+ this.options.vectorRanker.onEmbeddingPersisted({
664
+ entityId,
665
+ factId,
666
+ vector: vectorCopy
667
+ })
668
+ );
669
+ try {
670
+ await Promise.race([hookPromise, timeoutPromise]);
671
+ } catch (err) {
672
+ hookPromise.catch(() => {
673
+ });
674
+ throw err;
675
+ } finally {
676
+ if (timeoutHandle) clearTimeout(timeoutHandle);
677
+ }
579
678
  }
580
679
  async setup() {
581
680
  const entriesExistedBeforeSetup = await this.db.getFirstAsync(
@@ -627,6 +726,9 @@ var _WikiMemory = class _WikiMemory {
627
726
  );
628
727
  }
629
728
  }
729
+ if (entriesExistedBeforeSetup) {
730
+ await this.assertNoLegacySourceTypes();
731
+ }
630
732
  const rows = await this.db.getAllAsync(`
631
733
  SELECT rowid, source_ref FROM ${this.prefix}entries
632
734
  WHERE source_ref IS NOT NULL
@@ -759,33 +861,77 @@ var _WikiMemory = class _WikiMemory {
759
861
  let deletedEntries = 0;
760
862
  let deletedTasks = 0;
761
863
  let deletedEvents = 0;
762
- const deletedEntryIds = [];
763
864
  if (retainSoftDeletedFor !== null) {
764
865
  const cutoff = now - retainSoftDeletedFor * 864e5;
765
866
  const entriesToDelete = await this.db.getAllAsync(
766
- `SELECT id FROM ${this.prefix}entries
767
- WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at < ?`,
867
+ `SELECT id, entity_id FROM ${this.prefix}entries
868
+ WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at <= ?`,
768
869
  [entityId, cutoff]
769
870
  );
770
- deletedEntryIds.push(...entriesToDelete.map((e) => e.id));
771
- const entryResult = await this.db.runAsync(
772
- `DELETE FROM ${this.prefix}entries
773
- WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at < ?`,
774
- [entityId, cutoff]
775
- );
776
- deletedEntries = entryResult.changes;
871
+ const succeeded = [];
872
+ let failure = null;
873
+ for (const row of entriesToDelete) {
874
+ try {
875
+ await this._notifyEmbeddingPersistedOrThrow(row.entity_id, row.id, null);
876
+ succeeded.push({ entity_id: row.entity_id, id: row.id });
877
+ } catch (err) {
878
+ failure = { factId: row.id, cause: err };
879
+ break;
880
+ }
881
+ }
882
+ if (succeeded.length > 0) {
883
+ const chunkSize = 500;
884
+ for (let i = 0; i < succeeded.length; i += chunkSize) {
885
+ const chunk = succeeded.slice(i, i + chunkSize);
886
+ const placeholders = chunk.map(() => "?").join(",");
887
+ const entryResult = await this.db.runAsync(
888
+ `DELETE FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at <= ? AND id IN (${placeholders})`,
889
+ [entityId, cutoff, ...chunk.map((r) => r.id)]
890
+ );
891
+ deletedEntries += entryResult.changes;
892
+ }
893
+ }
777
894
  const taskResult = await this.db.runAsync(
778
895
  `DELETE FROM ${this.prefix}tasks
779
- WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at < ?`,
896
+ WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at <= ?`,
780
897
  [entityId, cutoff]
781
898
  );
782
899
  deletedTasks = taskResult.changes;
900
+ if (failure) {
901
+ await this.rebuildMiniSearchIndex(entityId);
902
+ this.vectorCache.delete(entityId);
903
+ const remaining = entriesToDelete.length - succeeded.length - 1;
904
+ const isTimeout = failure.cause?.[HOOK_TIMEOUT_MARKER] === true;
905
+ if (isTimeout) {
906
+ throw new PrunePartialFailureError(
907
+ succeeded.length,
908
+ failure.factId,
909
+ remaining,
910
+ new Error("Deletion hook timed out"),
911
+ deletedTasks,
912
+ 0
913
+ // events not yet deleted at this point
914
+ );
915
+ }
916
+ const errMsg = failure.cause?.message ?? "";
917
+ const isValidationError = errMsg.startsWith("Invalid deletionHookTimeoutMs");
918
+ const sanitizedCause = isValidationError ? failure.cause : this._sanitizeRankerError(failure.cause);
919
+ throw new PrunePartialFailureError(
920
+ succeeded.length,
921
+ failure.factId,
922
+ remaining,
923
+ sanitizedCause,
924
+ deletedTasks,
925
+ 0
926
+ // events not yet deleted at this point
927
+ );
928
+ }
783
929
  }
784
930
  if (retainEventsFor !== null) {
785
931
  const cutoff = now - retainEventsFor * 864e5;
786
932
  const eventResult = await this.db.runAsync(
787
933
  `DELETE FROM ${this.prefix}events
788
- WHERE entity_id = ? AND created_at < ?`,
934
+ WHERE entity_id = ? AND created_at <= ?`,
789
935
  [entityId, cutoff]
790
936
  );
791
937
  deletedEvents = eventResult.changes;
@@ -796,14 +942,6 @@ var _WikiMemory = class _WikiMemory {
796
942
  }
797
943
  await this.rebuildMiniSearchIndex(entityId);
798
944
  this.vectorCache.delete(entityId);
799
- const uniqueDeletedIds = Array.from(new Set(deletedEntryIds));
800
- for (const factId of uniqueDeletedIds) {
801
- try {
802
- await this._notifyEmbeddingPersisted(entityId, factId, null);
803
- } catch (hookErr) {
804
- console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during prune for ${factId}:`, hookErr);
805
- }
806
- }
807
945
  return { entries: deletedEntries, tasks: deletedTasks, events: deletedEvents };
808
946
  } finally {
809
947
  this.activeMaintenanceJobs.delete(pruneKey);
@@ -1057,7 +1195,10 @@ var _WikiMemory = class _WikiMemory {
1057
1195
  } catch (rankerErr) {
1058
1196
  const rankerError = rankerErr instanceof Error ? rankerErr : new Error(String(rankerErr));
1059
1197
  const policy = this.options.vectorRankerFallback ?? "js-cosine";
1060
- this.options.onVectorRankerFallback?.({ error: rankerError, policy });
1198
+ this.options.onVectorRankerFallback?.({
1199
+ error: this._sanitizeRankerError(rankerError),
1200
+ policy
1201
+ });
1061
1202
  if (policy === "throw") {
1062
1203
  rankerShouldRethrow = true;
1063
1204
  throw rankerError;
@@ -1121,8 +1262,9 @@ var _WikiMemory = class _WikiMemory {
1121
1262
  scored = [];
1122
1263
  }
1123
1264
  if (this.options.propagateRankerFailureToRetrievalFallback) {
1124
- const mirrored = new Error("Vector ranker failed, falling back");
1125
- mirrored.cause = rankerError;
1265
+ const mirrored = new Error("Vector ranker failed, falling back", {
1266
+ cause: this._sanitizeRankerError(rankerErr)
1267
+ });
1126
1268
  pendingRankerFallbackError = mirrored;
1127
1269
  }
1128
1270
  }
@@ -1282,12 +1424,31 @@ var _WikiMemory = class _WikiMemory {
1282
1424
  if (updatedAtDiff !== 0) return updatedAtDiff;
1283
1425
  return a.id.localeCompare(b.id);
1284
1426
  }
1427
+ /**
1428
+ * Strip potentially sensitive data from ranker errors before exposing to host callbacks.
1429
+ * Preserves error type for debugging but removes message/stack that may contain credentials.
1430
+ * Recursively sanitizes one level of .cause; deeper chains collapse to type only.
1431
+ */
1432
+ _sanitizeRankerError(err) {
1433
+ if (this.options.sanitizeRankerErrors === false) {
1434
+ return err instanceof Error ? err : new Error(String(err));
1435
+ }
1436
+ const typeName = err instanceof Error ? err.constructor?.name ?? "Error" : typeof err;
1437
+ const innerCause = err instanceof Error && err.cause !== void 0 ? new Error(`Caused by: ${err.cause?.constructor?.name ?? typeof err.cause}`) : void 0;
1438
+ const sanitized = new Error(
1439
+ `VectorRanker ${typeName} (message scrubbed for security)`,
1440
+ innerCause ? { cause: innerCause } : void 0
1441
+ );
1442
+ sanitized.name = typeName;
1443
+ return sanitized;
1444
+ }
1285
1445
  /**
1286
1446
  * Score candidate rows using in-process JS cosine similarity.
1287
1447
  * Applies hybrid blending (if weight set) and tie-break sorting before returning.
1288
1448
  */
1289
1449
  async _rankWithJsCosine(args) {
1290
- const { entityId, queryVec, candidateRows, weight, miniSearchScores, populateCache, limit } = args;
1450
+ const queryVec = args.queryVec instanceof Float32Array ? args.queryVec.slice() : Array.from(args.queryVec);
1451
+ const { entityId, candidateRows, weight, miniSearchScores, populateCache, limit } = args;
1291
1452
  let entityCache = this.vectorCache.get(entityId);
1292
1453
  const tooLarge = populateCache && candidateRows.length > _WikiMemory.MAX_VECTOR_CACHE_FACTS_PER_ENTITY;
1293
1454
  if (tooLarge && entityCache) {
@@ -1338,14 +1499,15 @@ var _WikiMemory = class _WikiMemory {
1338
1499
  * Returns scored results ready for hybrid blending and tie-break sorting.
1339
1500
  */
1340
1501
  async _rankWithVectorRanker(args) {
1341
- const { entityId, queryVec, candidateIds, weight, miniSearchScores, limit } = args;
1502
+ const { entityId, candidateIds, weight, miniSearchScores, limit } = args;
1342
1503
  const ranker = this.options.vectorRanker;
1343
1504
  if (!ranker) {
1344
1505
  throw new Error("vectorRanker not configured");
1345
1506
  }
1507
+ const queryVecCopy = args.queryVec instanceof Float32Array ? args.queryVec.slice() : Array.from(args.queryVec);
1346
1508
  const rankerResults = await ranker.rankBySimilarity({
1347
1509
  entityId,
1348
- queryVec,
1510
+ queryVec: queryVecCopy,
1349
1511
  candidateIds,
1350
1512
  limit
1351
1513
  });
@@ -1470,7 +1632,7 @@ ${JSON.stringify(currentFacts, null, 2)}`;
1470
1632
  let skip = false;
1471
1633
  if (newTokens.size >= MIN_TOKENS_TO_QUALIFY) {
1472
1634
  for (const existing of currentFactsRows) {
1473
- if (existing.source_type !== "agent_inferred") continue;
1635
+ if (existing.source_type !== "librarian_inferred") continue;
1474
1636
  const existingTokens = titleTokens(existing.title);
1475
1637
  if (existingTokens.size >= MIN_TOKENS_TO_QUALIFY) {
1476
1638
  if (jaccardScore(newTokens, existingTokens) >= FUZZY_THRESHOLD) {
@@ -1485,7 +1647,7 @@ ${JSON.stringify(currentFacts, null, 2)}`;
1485
1647
  await this.db.runAsync(`
1486
1648
  INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, created_at, updated_at)
1487
1649
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1488
- `, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "agent_inferred", now, now]);
1650
+ `, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "librarian_inferred", now, now]);
1489
1651
  insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
1490
1652
  }
1491
1653
  for (const task of validTasks) {
@@ -1518,25 +1680,25 @@ ${JSON.stringify(currentFacts, null, 2)}`;
1518
1680
  if (orphanAfterDays !== null) {
1519
1681
  const orphanThreshold = now - orphanAfterDays * MS_PER_DAY;
1520
1682
  await this.db.runAsync(`
1521
- UPDATE ${this.prefix}entries
1522
- SET deleted_at = ?, updated_at = ?
1523
- WHERE entity_id = ? AND access_count = 0 AND created_at < ? AND source_type != 'user_document' AND deleted_at IS NULL
1683
+ UPDATE ${this.prefix}entries
1684
+ SET deleted_at = ?, updated_at = ?
1685
+ WHERE entity_id = ? AND access_count = 0 AND created_at <= ? AND source_type != 'immutable_document' AND deleted_at IS NULL
1524
1686
  `, [now, now, entityId, orphanThreshold]);
1525
1687
  }
1526
1688
  if (staleInferredAfterDays !== null) {
1527
1689
  const staleThreshold = now - staleInferredAfterDays * MS_PER_DAY;
1528
1690
  await this.db.runAsync(`
1529
- UPDATE ${this.prefix}entries
1530
- SET confidence = 'tentative', updated_at = ?
1531
- 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
1691
+ UPDATE ${this.prefix}entries
1692
+ SET confidence = 'tentative', updated_at = ?
1693
+ 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
1532
1694
  `, [now, entityId, staleThreshold, staleThreshold]);
1533
1695
  }
1534
1696
  });
1535
1697
  const allFactsRows = await this.db.getAllAsync(`SELECT * FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`, [entityId]);
1536
1698
  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]);
1537
1699
  const recentEvents = await this.db.getAllAsync(`SELECT * FROM ${this.prefix}events WHERE entity_id = ? ORDER BY created_at DESC LIMIT 20`, [entityId]);
1538
- const healCandidates = allFactsRows.filter((f) => f.source_type !== "user_document");
1539
- const documentAnchors = allFactsRows.filter((f) => f.source_type === "user_document").map(({ id, title, source_ref }) => ({ id, title, source_ref }));
1700
+ const healCandidates = allFactsRows.filter((f) => f.source_type !== "immutable_document");
1701
+ const documentAnchors = allFactsRows.filter((f) => f.source_type === "immutable_document").map(({ id, title, source_ref }) => ({ id, title, source_ref }));
1540
1702
  const userPrompt = `Heal Candidates:
1541
1703
  ${JSON.stringify(healCandidates.map((f) => {
1542
1704
  const { embedding: _embedding, embedding_blob: _blob, ...rest } = f;
@@ -1579,7 +1741,7 @@ The following document anchors are provided for contradiction detection only. Do
1579
1741
  await this.db.runAsync(`
1580
1742
  INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, created_at, updated_at)
1581
1743
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1582
- `, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "agent_inferred", now, now]);
1744
+ `, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "librarian_inferred", now, now]);
1583
1745
  insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
1584
1746
  }
1585
1747
  });
@@ -1879,6 +2041,7 @@ The following document anchors are provided for contradiction detection only. Do
1879
2041
  this.activeMaintenanceJobs.add(this._importKey(entityId));
1880
2042
  }
1881
2043
  try {
2044
+ await this.assertNoLegacySourceTypes();
1882
2045
  for (const [entityId, bundle] of Object.entries(dump.entities)) {
1883
2046
  await this._doImportEntity(entityId, bundle, merge);
1884
2047
  }
@@ -1932,6 +2095,10 @@ The following document anchors are provided for contradiction detection only. Do
1932
2095
  }
1933
2096
  }
1934
2097
  for (const fact of bundle.facts) {
2098
+ const sourceType = this._normalizeImportedSourceType(String(fact.source_type), {
2099
+ entityId,
2100
+ factId: fact.id
2101
+ });
1935
2102
  const tagsJson = JSON.stringify(Array.isArray(fact.tags) ? fact.tags : []);
1936
2103
  const safeUpdatedAt = Number.isFinite(fact.updated_at) ? fact.updated_at : 0;
1937
2104
  const existing = existingFactsById.get(fact.id);
@@ -1980,14 +2147,14 @@ The following document anchors are provided for contradiction detection only. Do
1980
2147
  if (blobData != null) {
1981
2148
  await this.db.runAsync(
1982
2149
  `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 = ?`,
1983
- [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]
2150
+ [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]
1984
2151
  );
1985
2152
  factsWithPreservedBlob.set(fact.id, blobData);
1986
2153
  if (!fact.deleted_at) preservedBlobDims.add(blobData.byteLength / 4);
1987
2154
  } else {
1988
2155
  await this.db.runAsync(
1989
2156
  `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 = ?`,
1990
- [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]
2157
+ [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]
1991
2158
  );
1992
2159
  }
1993
2160
  existingFactsById.set(fact.id, { id: fact.id, entity_id: entityId, updated_at: safeUpdatedAt });
@@ -1997,14 +2164,14 @@ The following document anchors are provided for contradiction detection only. Do
1997
2164
  if (blobData != null) {
1998
2165
  await this.db.runAsync(
1999
2166
  `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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2000
- [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]
2167
+ [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]
2001
2168
  );
2002
2169
  factsWithPreservedBlob.set(fact.id, blobData);
2003
2170
  if (!fact.deleted_at) preservedBlobDims.add(blobData.byteLength / 4);
2004
2171
  } else {
2005
2172
  await this.db.runAsync(
2006
2173
  `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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2007
- [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]
2174
+ [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]
2008
2175
  );
2009
2176
  }
2010
2177
  existingFactsById.set(fact.id, { id: fact.id, entity_id: entityId, updated_at: safeUpdatedAt });
@@ -2165,11 +2332,15 @@ The following document anchors are provided for contradiction detection only. Do
2165
2332
  let deletedTasks = 0;
2166
2333
  const deletedEntryIds = [];
2167
2334
  if (params.clearAll) {
2168
- const entriesToDelete = await this.db.getAllAsync(
2335
+ const newDeletions = await this.db.getAllAsync(
2169
2336
  `SELECT id FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`,
2170
2337
  [entityId]
2171
2338
  );
2172
- deletedEntryIds.push(...entriesToDelete.map((e) => e.id));
2339
+ const alreadySoftDeleted = await this.db.getAllAsync(
2340
+ `SELECT id FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NOT NULL`,
2341
+ [entityId]
2342
+ );
2343
+ deletedEntryIds.push(...newDeletions.map((e) => e.id), ...alreadySoftDeleted.map((e) => e.id));
2173
2344
  const [entriesRes, tasksRes] = await Promise.all([
2174
2345
  this.db.runAsync(`UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE entity_id = ? AND deleted_at IS NULL`, [now, now, entityId]),
2175
2346
  this.db.runAsync(`UPDATE ${this.prefix}tasks SET deleted_at = ?, updated_at = ? WHERE entity_id = ? AND deleted_at IS NULL`, [now, now, entityId])
@@ -2189,13 +2360,13 @@ The following document anchors are provided for contradiction detection only. Do
2189
2360
  if (params.sourceHash !== void 0 && !sourceHash) throw new Error("Invalid sourceHash (must be 64-char hex string)");
2190
2361
  if (params.entryId) {
2191
2362
  const entry = await this.db.getFirstAsync(
2192
- `SELECT id FROM ${this.prefix}entries WHERE id = ? AND entity_id = ? AND deleted_at IS NULL`,
2363
+ `SELECT id FROM ${this.prefix}entries WHERE id = ? AND entity_id = ?`,
2193
2364
  [params.entryId, entityId]
2194
2365
  );
2195
2366
  if (entry) deletedEntryIds.push(entry.id);
2196
2367
  }
2197
2368
  if (sourceRef || sourceHash) {
2198
- let q = `SELECT id FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`;
2369
+ let q = `SELECT id FROM ${this.prefix}entries WHERE entity_id = ?`;
2199
2370
  const args = [entityId];
2200
2371
  if (sourceRef) {
2201
2372
  q += ` AND source_ref = ?`;
@@ -2238,9 +2409,26 @@ The following document anchors are provided for contradiction detection only. Do
2238
2409
  const uniqueDeletedIds = Array.from(new Set(deletedEntryIds));
2239
2410
  for (const factId of uniqueDeletedIds) {
2240
2411
  try {
2241
- await this._notifyEmbeddingPersisted(entityId, factId, null);
2412
+ await this._notifyEmbeddingPersistedOrThrow(entityId, factId, null);
2242
2413
  } catch (hookErr) {
2243
- console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during forget for ${factId}:`, hookErr);
2414
+ const isTimeout = hookErr?.[HOOK_TIMEOUT_MARKER] === true;
2415
+ if (isTimeout) {
2416
+ throw new Error(
2417
+ `forget(${entityId}/${factId}) failed: ${hookErr.message}`
2418
+ );
2419
+ }
2420
+ const errMsg = hookErr?.message ?? "";
2421
+ const isValidationError = errMsg.startsWith("Invalid deletionHookTimeoutMs");
2422
+ if (isValidationError) {
2423
+ throw new Error(
2424
+ `forget(${entityId}/${factId}) failed: ${errMsg}`,
2425
+ { cause: hookErr }
2426
+ );
2427
+ }
2428
+ throw new Error(
2429
+ `forget(${entityId}/${factId}) failed: ANN cleanup hook rejected`,
2430
+ { cause: this._sanitizeRankerError(hookErr) }
2431
+ );
2244
2432
  }
2245
2433
  }
2246
2434
  return { deleted: { entries: deletedEntries, tasks: deletedTasks } };
@@ -2330,7 +2518,7 @@ ${chunk}`;
2330
2518
  await this.db.runAsync(
2331
2519
  `INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, source_hash, source_ref, created_at, updated_at)
2332
2520
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2333
- [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "user_document", sourceHash, sourceRef, now, now]
2521
+ [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "immutable_document", sourceHash, sourceRef, now, now]
2334
2522
  );
2335
2523
  insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
2336
2524
  }
@@ -2589,6 +2777,6 @@ function createWiki(db, options) {
2589
2777
  return new WikiMemory(db, options);
2590
2778
  }
2591
2779
 
2592
- export { WikiBusyError, WikiMemory, createWiki, formatContext, formatMemoryDump };
2780
+ export { PrunePartialFailureError, WikiBusyError, WikiMemory, createWiki, formatContext, formatMemoryDump, parseEmbedding };
2593
2781
  //# sourceMappingURL=index.mjs.map
2594
2782
  //# sourceMappingURL=index.mjs.map