@equationalapplications/core-llm-wiki 3.0.0 → 3.2.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
@@ -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("[");
@@ -554,6 +567,11 @@ var _WikiMemory = class _WikiMemory {
554
567
  `UPDATE ${this.prefix}entries SET embedding_blob = ?, embedding = NULL WHERE id = ?`,
555
568
  [blob, fact.id]
556
569
  );
570
+ try {
571
+ await this._notifyEmbeddingPersisted(fact.entity_id, fact.id, float32Vector);
572
+ } catch (hookErr) {
573
+ console.warn(`[WikiMemory] onEmbeddingPersisted hook failed for ${fact.id}:`, hookErr);
574
+ }
557
575
  return true;
558
576
  } catch (err) {
559
577
  console.warn(`[WikiMemory] embedFact failed for ${fact.id}:`, err);
@@ -569,6 +587,57 @@ var _WikiMemory = class _WikiMemory {
569
587
  _warnCrossEntityCollision(type, id, existingEntityId, targetEntityId) {
570
588
  console.warn(`[WikiMemory] importDump: ${type} id "${id}" already belongs to entity "${existingEntityId}"; skipping for entity "${targetEntityId}"`);
571
589
  }
590
+ async _notifyEmbeddingPersisted(entityId, factId, vector) {
591
+ if (!this.options.vectorRanker?.onEmbeddingPersisted) return;
592
+ const vectorCopy = vector ? vector.slice() : null;
593
+ await this.options.vectorRanker.onEmbeddingPersisted({
594
+ entityId,
595
+ factId,
596
+ vector: vectorCopy
597
+ });
598
+ }
599
+ /**
600
+ * GDPR-critical variant: awaits the hook with a timeout and rethrows failures.
601
+ * Use ONLY on deletion paths. forget() calls after soft-delete UPDATE; runPrune()
602
+ * calls before hard DELETE. For best-effort sync, use _notifyEmbeddingPersisted.
603
+ */
604
+ async _notifyEmbeddingPersistedOrThrow(entityId, factId, vector) {
605
+ if (!this.options.vectorRanker?.onEmbeddingPersisted) return;
606
+ if (this.options.forceDeleteIgnoreRankerHook === true) return;
607
+ const vectorCopy = vector ? vector.slice() : null;
608
+ const rawTimeout = this.options.deletionHookTimeoutMs ?? 3e4;
609
+ if (typeof rawTimeout !== "number" || !Number.isFinite(rawTimeout) || rawTimeout <= 0) {
610
+ throw new Error("Invalid deletionHookTimeoutMs: must be a positive finite number");
611
+ }
612
+ const timeoutMs = rawTimeout;
613
+ let timeoutHandle;
614
+ const timeoutPromise = new Promise((_, reject) => {
615
+ timeoutHandle = setTimeout(
616
+ () => {
617
+ const timeoutError = new Error(`onEmbeddingPersisted timed out after ${timeoutMs}ms`);
618
+ timeoutError[HOOK_TIMEOUT_MARKER] = true;
619
+ reject(timeoutError);
620
+ },
621
+ timeoutMs
622
+ );
623
+ });
624
+ const hookPromise = Promise.resolve(
625
+ this.options.vectorRanker.onEmbeddingPersisted({
626
+ entityId,
627
+ factId,
628
+ vector: vectorCopy
629
+ })
630
+ );
631
+ try {
632
+ await Promise.race([hookPromise, timeoutPromise]);
633
+ } catch (err) {
634
+ hookPromise.catch(() => {
635
+ });
636
+ throw err;
637
+ } finally {
638
+ if (timeoutHandle) clearTimeout(timeoutHandle);
639
+ }
640
+ }
572
641
  async setup() {
573
642
  const entriesExistedBeforeSetup = await this.db.getFirstAsync(
574
643
  `SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
@@ -753,18 +822,69 @@ var _WikiMemory = class _WikiMemory {
753
822
  let deletedEvents = 0;
754
823
  if (retainSoftDeletedFor !== null) {
755
824
  const cutoff = now - retainSoftDeletedFor * 864e5;
756
- const entryResult = await this.db.runAsync(
757
- `DELETE FROM ${this.prefix}entries
825
+ const entriesToDelete = await this.db.getAllAsync(
826
+ `SELECT id, entity_id FROM ${this.prefix}entries
758
827
  WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at < ?`,
759
828
  [entityId, cutoff]
760
829
  );
761
- deletedEntries = entryResult.changes;
830
+ const succeeded = [];
831
+ let failure = null;
832
+ for (const row of entriesToDelete) {
833
+ try {
834
+ await this._notifyEmbeddingPersistedOrThrow(row.entity_id, row.id, null);
835
+ succeeded.push({ entity_id: row.entity_id, id: row.id });
836
+ } catch (err) {
837
+ failure = { factId: row.id, cause: err };
838
+ break;
839
+ }
840
+ }
841
+ if (succeeded.length > 0) {
842
+ const chunkSize = 500;
843
+ for (let i = 0; i < succeeded.length; i += chunkSize) {
844
+ const chunk = succeeded.slice(i, i + chunkSize);
845
+ const placeholders = chunk.map(() => "?").join(",");
846
+ const entryResult = await this.db.runAsync(
847
+ `DELETE FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at < ? AND id IN (${placeholders})`,
848
+ [entityId, cutoff, ...chunk.map((r) => r.id)]
849
+ );
850
+ deletedEntries += entryResult.changes;
851
+ }
852
+ }
762
853
  const taskResult = await this.db.runAsync(
763
854
  `DELETE FROM ${this.prefix}tasks
764
855
  WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at < ?`,
765
856
  [entityId, cutoff]
766
857
  );
767
858
  deletedTasks = taskResult.changes;
859
+ if (failure) {
860
+ await this.rebuildMiniSearchIndex(entityId);
861
+ this.vectorCache.delete(entityId);
862
+ const remaining = entriesToDelete.length - succeeded.length - 1;
863
+ const isTimeout = failure.cause?.[HOOK_TIMEOUT_MARKER] === true;
864
+ if (isTimeout) {
865
+ throw new PrunePartialFailureError(
866
+ succeeded.length,
867
+ failure.factId,
868
+ remaining,
869
+ new Error("Deletion hook timed out"),
870
+ deletedTasks,
871
+ 0
872
+ // events not yet deleted at this point
873
+ );
874
+ }
875
+ const errMsg = failure.cause?.message ?? "";
876
+ const isValidationError = errMsg.startsWith("Invalid deletionHookTimeoutMs");
877
+ const sanitizedCause = isValidationError ? failure.cause : this._sanitizeRankerError(failure.cause);
878
+ throw new PrunePartialFailureError(
879
+ succeeded.length,
880
+ failure.factId,
881
+ remaining,
882
+ sanitizedCause,
883
+ deletedTasks,
884
+ 0
885
+ // events not yet deleted at this point
886
+ );
887
+ }
768
888
  }
769
889
  if (retainEventsFor !== null) {
770
890
  const cutoff = now - retainEventsFor * 864e5;
@@ -801,6 +921,10 @@ var _WikiMemory = class _WikiMemory {
801
921
  if (maxResults === 0) ; else if (trimmedQuery) {
802
922
  let usedEmbed = false;
803
923
  if (!skipEmbed && embedFn) {
924
+ let rankerShouldRethrow = false;
925
+ let pendingRankerFallbackError;
926
+ let usedKeywordFallback = false;
927
+ let scoredAlreadySortedAndLimited = false;
804
928
  try {
805
929
  const queryVec = await embedFn(trimmedQuery);
806
930
  if (queryVec.length === 0 || !queryVec.every((v) => typeof v === "number" && isFinite(v))) {
@@ -832,6 +956,7 @@ var _WikiMemory = class _WikiMemory {
832
956
  `Some facts have embeddings that do not match the current model dimension. Call runReembed() to rebuild all embeddings consistently.`
833
957
  );
834
958
  }
959
+ const useRanker = Boolean(this.options.vectorRanker);
835
960
  let candidateRows;
836
961
  let populateCache = true;
837
962
  let miniSearchScores;
@@ -850,15 +975,30 @@ var _WikiMemory = class _WikiMemory {
850
975
  } else {
851
976
  const topKIds = topKResults.map((r) => r.id);
852
977
  const inClauseChunkSize = 500;
853
- candidateRows = [];
854
- for (let i = 0; i < topKIds.length; i += inClauseChunkSize) {
855
- const idChunk = topKIds.slice(i, i + inClauseChunkSize);
856
- const placeholders = idChunk.map(() => "?").join(",");
857
- const chunkRows = await this.db.getAllAsync(
858
- `SELECT id, embedding_blob, embedding, updated_at, access_count FROM ${this.prefix}entries WHERE id IN (${placeholders}) AND deleted_at IS NULL`,
859
- idChunk
860
- );
861
- candidateRows.push(...chunkRows);
978
+ if (useRanker) {
979
+ const rows = [];
980
+ for (let i = 0; i < topKIds.length; i += inClauseChunkSize) {
981
+ const idChunk = topKIds.slice(i, i + inClauseChunkSize);
982
+ const placeholders = idChunk.map(() => "?").join(",");
983
+ const chunkRows = await this.db.getAllAsync(
984
+ `SELECT id, updated_at, access_count FROM ${this.prefix}entries WHERE id IN (${placeholders}) AND deleted_at IS NULL`,
985
+ idChunk
986
+ );
987
+ rows.push(...chunkRows);
988
+ }
989
+ candidateRows = rows;
990
+ } else {
991
+ const rows = [];
992
+ for (let i = 0; i < topKIds.length; i += inClauseChunkSize) {
993
+ const idChunk = topKIds.slice(i, i + inClauseChunkSize);
994
+ const placeholders = idChunk.map(() => "?").join(",");
995
+ const chunkRows = await this.db.getAllAsync(
996
+ `SELECT id, embedding_blob, embedding, updated_at, access_count FROM ${this.prefix}entries WHERE id IN (${placeholders}) AND deleted_at IS NULL`,
997
+ idChunk
998
+ );
999
+ rows.push(...chunkRows);
1000
+ }
1001
+ candidateRows = rows;
862
1002
  }
863
1003
  if (weight !== void 0 && weight < 1) {
864
1004
  const maxMsScore = Math.max(1, topKResults[0]?.score ?? 1);
@@ -867,10 +1007,17 @@ var _WikiMemory = class _WikiMemory {
867
1007
  }
868
1008
  }
869
1009
  } else {
870
- candidateRows = await this.db.getAllAsync(
871
- `SELECT id, embedding_blob, embedding, updated_at, access_count FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`,
872
- [entityId]
873
- );
1010
+ if (useRanker) {
1011
+ candidateRows = await this.db.getAllAsync(
1012
+ `SELECT id, updated_at, access_count FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`,
1013
+ [entityId]
1014
+ );
1015
+ } else {
1016
+ candidateRows = await this.db.getAllAsync(
1017
+ `SELECT id, embedding_blob, embedding, updated_at, access_count FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`,
1018
+ [entityId]
1019
+ );
1020
+ }
874
1021
  if (weight !== void 0 && weight < 1) {
875
1022
  const msResults = this.miniSearch.search(trimmedQuery, {
876
1023
  filter: (r) => r.entity_id === entityId,
@@ -883,76 +1030,267 @@ var _WikiMemory = class _WikiMemory {
883
1030
  if (candidateRows === null) {
884
1031
  usedEmbed = true;
885
1032
  } else {
886
- let entityCache = this.vectorCache.get(entityId);
887
- const tooLarge = populateCache && candidateRows.length > _WikiMemory.MAX_VECTOR_CACHE_FACTS_PER_ENTITY;
888
- if (tooLarge && entityCache) {
889
- this.vectorCache.delete(entityId);
890
- entityCache = void 0;
891
- }
892
- const canCache = populateCache && !tooLarge;
893
- if (canCache && !entityCache) {
894
- entityCache = /* @__PURE__ */ new Map();
895
- }
896
- const scored = candidateRows.map((row) => {
897
- let vector = entityCache?.get(row.id) ?? parseEmbedding(row.embedding_blob, row.embedding);
898
- if (vector && canCache && entityCache && !entityCache.has(row.id)) {
899
- entityCache.set(row.id, vector);
900
- }
901
- let score = 0;
902
- if (vector && vector.length === queryVec.length) {
903
- const cosSim = cosineSimilarity(queryVec, vector);
904
- if (weight !== void 0) {
905
- const kwScore = miniSearchScores?.get(row.id) ?? 0;
906
- score = weight * Math.max(0, cosSim) + (1 - weight) * kwScore;
1033
+ let scored;
1034
+ if (useRanker) {
1035
+ const candidateIds = effectivePreFilterLimit !== void 0 ? candidateRows.map((r) => r.id) : void 0;
1036
+ try {
1037
+ const oversampledLimit = Math.max(maxResults * 2, maxResults + 50);
1038
+ scored = await this._rankWithVectorRanker({
1039
+ entityId,
1040
+ queryVec,
1041
+ candidateIds,
1042
+ weight,
1043
+ miniSearchScores,
1044
+ limit: oversampledLimit
1045
+ });
1046
+ if (scored.length > 0) {
1047
+ const scoredIds2 = new Set(scored.map((s) => s.id));
1048
+ const metaMap = /* @__PURE__ */ new Map();
1049
+ for (const r of candidateRows) {
1050
+ if (scoredIds2.has(r.id)) {
1051
+ metaMap.set(r.id, { updated_at: r.updated_at, access_count: r.access_count });
1052
+ }
1053
+ }
1054
+ scored = scored.map((s) => {
1055
+ const meta = metaMap.get(s.id);
1056
+ return { ...s, updated_at: meta?.updated_at ?? null, access_count: meta?.access_count ?? null };
1057
+ });
1058
+ }
1059
+ const scoredIds = new Set(scored.map((s) => s.id));
1060
+ const isHybrid = weight !== void 0 && weight < 1;
1061
+ const maxBackfill = isHybrid ? maxResults : Math.max(0, maxResults - scored.length);
1062
+ if (maxBackfill > 0) {
1063
+ if (isHybrid) {
1064
+ const topK = [];
1065
+ for (const row of candidateRows) {
1066
+ if (scoredIds.has(row.id)) continue;
1067
+ const kwScore = miniSearchScores?.get(row.id) ?? 0;
1068
+ const candidate = { row, kwScore };
1069
+ if (topK.length < maxBackfill) {
1070
+ let insertIdx = topK.length;
1071
+ for (let i = 0; i < topK.length; i++) {
1072
+ const cmp = this._compareScoredRows(
1073
+ {
1074
+ id: candidate.row.id,
1075
+ score: candidate.kwScore,
1076
+ updated_at: candidate.row.updated_at,
1077
+ access_count: candidate.row.access_count
1078
+ },
1079
+ {
1080
+ id: topK[i].row.id,
1081
+ score: topK[i].kwScore,
1082
+ updated_at: topK[i].row.updated_at,
1083
+ access_count: topK[i].row.access_count
1084
+ }
1085
+ );
1086
+ if (cmp < 0) {
1087
+ insertIdx = i;
1088
+ break;
1089
+ }
1090
+ }
1091
+ topK.splice(insertIdx, 0, candidate);
1092
+ } else {
1093
+ const cmpWorst = this._compareScoredRows(
1094
+ {
1095
+ id: candidate.row.id,
1096
+ score: candidate.kwScore,
1097
+ updated_at: candidate.row.updated_at,
1098
+ access_count: candidate.row.access_count
1099
+ },
1100
+ {
1101
+ id: topK[maxBackfill - 1].row.id,
1102
+ score: topK[maxBackfill - 1].kwScore,
1103
+ updated_at: topK[maxBackfill - 1].row.updated_at,
1104
+ access_count: topK[maxBackfill - 1].row.access_count
1105
+ }
1106
+ );
1107
+ if (cmpWorst < 0) {
1108
+ let insertIdx = maxBackfill - 1;
1109
+ for (let i = 0; i < topK.length; i++) {
1110
+ const cmp = this._compareScoredRows(
1111
+ {
1112
+ id: candidate.row.id,
1113
+ score: candidate.kwScore,
1114
+ updated_at: candidate.row.updated_at,
1115
+ access_count: candidate.row.access_count
1116
+ },
1117
+ {
1118
+ id: topK[i].row.id,
1119
+ score: topK[i].kwScore,
1120
+ updated_at: topK[i].row.updated_at,
1121
+ access_count: topK[i].row.access_count
1122
+ }
1123
+ );
1124
+ if (cmp < 0) {
1125
+ insertIdx = i;
1126
+ break;
1127
+ }
1128
+ }
1129
+ topK.splice(insertIdx, 0, candidate);
1130
+ topK.pop();
1131
+ }
1132
+ }
1133
+ }
1134
+ for (const { row, kwScore } of topK) {
1135
+ scored.push({
1136
+ id: row.id,
1137
+ score: (1 - weight) * kwScore,
1138
+ updated_at: row.updated_at,
1139
+ access_count: row.access_count
1140
+ });
1141
+ }
1142
+ } else {
1143
+ const omitted = [];
1144
+ for (const row of candidateRows) {
1145
+ if (scoredIds.has(row.id)) continue;
1146
+ omitted.push({ id: row.id, score: -2, updated_at: row.updated_at, access_count: row.access_count });
1147
+ }
1148
+ if (omitted.length > 0) {
1149
+ this._tieBreakSort(omitted);
1150
+ scored.push(...omitted.slice(0, maxBackfill));
1151
+ }
1152
+ }
1153
+ }
1154
+ } catch (rankerErr) {
1155
+ const rankerError = rankerErr instanceof Error ? rankerErr : new Error(String(rankerErr));
1156
+ const policy = this.options.vectorRankerFallback ?? "js-cosine";
1157
+ this.options.onVectorRankerFallback?.({
1158
+ error: this._sanitizeRankerError(rankerError),
1159
+ policy
1160
+ });
1161
+ if (policy === "throw") {
1162
+ rankerShouldRethrow = true;
1163
+ throw rankerError;
1164
+ } else if (policy === "js-cosine") {
1165
+ let fallbackRows = candidateRows;
1166
+ if (fallbackRows && fallbackRows.length > 0 && !("embedding_blob" in fallbackRows[0])) {
1167
+ const rowIds = fallbackRows.map((r) => r.id);
1168
+ const embeddingsMap = /* @__PURE__ */ new Map();
1169
+ const chunkSize = 500;
1170
+ for (let i = 0; i < rowIds.length; i += chunkSize) {
1171
+ const idChunk = rowIds.slice(i, i + chunkSize);
1172
+ const placeholders = idChunk.map(() => "?").join(",");
1173
+ const embeddingRows = await this.db.getAllAsync(
1174
+ `SELECT id, embedding_blob, embedding FROM ${this.prefix}entries WHERE id IN (${placeholders}) AND entity_id = ? AND deleted_at IS NULL`,
1175
+ [...idChunk, entityId]
1176
+ );
1177
+ for (const row of embeddingRows) {
1178
+ embeddingsMap.set(row.id, { embedding_blob: row.embedding_blob, embedding: row.embedding });
1179
+ }
1180
+ }
1181
+ fallbackRows = fallbackRows.map((r) => ({
1182
+ ...r,
1183
+ embedding_blob: embeddingsMap.get(r.id)?.embedding_blob ?? null,
1184
+ embedding: embeddingsMap.get(r.id)?.embedding ?? null
1185
+ }));
1186
+ }
1187
+ scored = await this._rankWithJsCosine({
1188
+ entityId,
1189
+ queryVec,
1190
+ candidateRows: fallbackRows,
1191
+ weight,
1192
+ miniSearchScores,
1193
+ populateCache,
1194
+ limit: maxResults
1195
+ });
1196
+ scoredAlreadySortedAndLimited = true;
1197
+ } else if (policy === "keyword") {
1198
+ const msResults = this.miniSearch.search(trimmedQuery, {
1199
+ filter: (r) => r.entity_id === entityId,
1200
+ combineWith: "OR"
1201
+ });
1202
+ const topResults = msResults.slice(0, maxResults);
1203
+ const resultIds = new Set(topResults.map((r) => r.id));
1204
+ const candidateMap = /* @__PURE__ */ new Map();
1205
+ for (const r of candidateRows) {
1206
+ if (resultIds.has(r.id)) {
1207
+ candidateMap.set(r.id, { updated_at: r.updated_at, access_count: r.access_count });
1208
+ }
1209
+ }
1210
+ scored = topResults.map((r) => {
1211
+ const meta = candidateMap.get(r.id);
1212
+ return {
1213
+ id: r.id,
1214
+ score: r.score ?? 0,
1215
+ access_count: meta?.access_count ?? null,
1216
+ updated_at: meta?.updated_at ?? null
1217
+ };
1218
+ });
1219
+ usedKeywordFallback = true;
907
1220
  } else {
908
- score = cosSim;
1221
+ scored = [];
909
1222
  }
910
- } else if (weight !== void 0 && weight < 1) {
911
- const kwScore = miniSearchScores?.get(row.id) ?? 0;
912
- score = (1 - weight) * kwScore;
913
- } else {
914
- score = -2;
915
- }
916
- return { row, score };
917
- });
918
- if (canCache && entityCache && entityCache.size > 0) {
919
- if (!this.vectorCache.has(entityId)) {
920
- if (this.vectorCache.size >= _WikiMemory.MAX_VECTOR_CACHE_ENTITIES) {
921
- const oldestKey = this.vectorCache.keys().next().value;
922
- if (oldestKey !== void 0) this.vectorCache.delete(oldestKey);
1223
+ if (this.options.propagateRankerFailureToRetrievalFallback) {
1224
+ const mirrored = new Error("Vector ranker failed, falling back", {
1225
+ cause: this._sanitizeRankerError(rankerErr)
1226
+ });
1227
+ pendingRankerFallbackError = mirrored;
923
1228
  }
924
- this.vectorCache.set(entityId, entityCache);
925
1229
  }
1230
+ } else {
1231
+ scored = await this._rankWithJsCosine({
1232
+ entityId,
1233
+ queryVec,
1234
+ candidateRows,
1235
+ weight,
1236
+ miniSearchScores,
1237
+ populateCache,
1238
+ limit: maxResults
1239
+ });
1240
+ scoredAlreadySortedAndLimited = true;
926
1241
  }
927
- scored.sort((a, b) => {
928
- const scoreDiff = b.score - a.score;
929
- if (scoreDiff !== 0) return scoreDiff;
930
- const accessCountDiff = (b.row.access_count ?? 0) - (a.row.access_count ?? 0);
931
- if (accessCountDiff !== 0) return accessCountDiff;
932
- const updatedAtDiff = (b.row.updated_at ?? 0) - (a.row.updated_at ?? 0);
933
- if (updatedAtDiff !== 0) return updatedAtDiff;
934
- return a.row.id.localeCompare(b.row.id);
935
- });
936
- const topIds = scored.slice(0, maxResults).map((s) => s.row.id);
937
- if (topIds.length > 0) {
938
- const fullRows = [];
939
- const phase2ChunkSize = 500;
940
- for (let i = 0; i < topIds.length; i += phase2ChunkSize) {
941
- const idChunk = topIds.slice(i, i + phase2ChunkSize);
942
- const placeholders = idChunk.map(() => "?").join(",");
943
- const chunkRows = await this.db.getAllAsync(
944
- `SELECT * FROM ${this.prefix}entries WHERE id IN (${placeholders}) AND deleted_at IS NULL`,
945
- idChunk
946
- );
947
- fullRows.push(...chunkRows);
1242
+ if (scored.length > 0) {
1243
+ if (!usedKeywordFallback && !scoredAlreadySortedAndLimited) {
1244
+ this._tieBreakSort(scored);
948
1245
  }
949
- const byId = new Map(fullRows.map((r) => [r.id, r]));
950
- facts = topIds.map((id) => byId.get(id)).filter((f) => f !== void 0);
1246
+ const topIds = (scoredAlreadySortedAndLimited ? scored : scored.slice(0, maxResults)).map((s) => s.id);
1247
+ if (topIds.length > 0) {
1248
+ const fullRows = [];
1249
+ const phase2ChunkSize = 500;
1250
+ for (let i = 0; i < topIds.length; i += phase2ChunkSize) {
1251
+ const idChunk = topIds.slice(i, i + phase2ChunkSize);
1252
+ const placeholders = idChunk.map(() => "?").join(",");
1253
+ const chunkRows = await this.db.getAllAsync(
1254
+ `SELECT * FROM ${this.prefix}entries WHERE id IN (${placeholders}) AND entity_id = ? AND deleted_at IS NULL`,
1255
+ [...idChunk, entityId]
1256
+ );
1257
+ fullRows.push(...chunkRows);
1258
+ }
1259
+ const byId = new Map(fullRows.map((r) => [r.id, r]));
1260
+ facts = topIds.map((id) => byId.get(id)).filter((f) => f !== void 0);
1261
+ if (facts.length < topIds.length) {
1262
+ const missingIds = topIds.filter((id) => !byId.has(id));
1263
+ const missingCount = missingIds.length;
1264
+ const sample = missingIds.slice(0, 5);
1265
+ const sampleSuffix = sample.length > 0 ? ` Missing ID sample: ${sample.join(", ")}${missingIds.length > sample.length ? ", ..." : ""}.` : "";
1266
+ const error = new Error(
1267
+ `Phase 2 fact hydration returned ${missingCount} fewer row(s) than ranked IDs for entity ${entityId}. Rows may have been concurrently soft-deleted or filtered by deleted_at during hydration, or vector ranker output may include IDs that do not exist for this entity.` + sampleSuffix
1268
+ );
1269
+ this.options.onRetrievalFallback?.(error);
1270
+ }
1271
+ }
1272
+ if (pendingRankerFallbackError) {
1273
+ this.options.onRetrievalFallback?.(pendingRankerFallbackError);
1274
+ pendingRankerFallbackError = void 0;
1275
+ }
1276
+ usedEmbed = true;
1277
+ } else {
1278
+ if (pendingRankerFallbackError) {
1279
+ this.options.onRetrievalFallback?.(pendingRankerFallbackError);
1280
+ pendingRankerFallbackError = void 0;
1281
+ }
1282
+ usedEmbed = true;
951
1283
  }
952
- usedEmbed = true;
953
1284
  }
954
1285
  } catch (err) {
955
1286
  const error = err instanceof Error ? err : new Error(String(err));
1287
+ if (rankerShouldRethrow) {
1288
+ throw error;
1289
+ }
1290
+ if (pendingRankerFallbackError) {
1291
+ error.cause = pendingRankerFallbackError;
1292
+ pendingRankerFallbackError = void 0;
1293
+ }
956
1294
  this.options.onRetrievalFallback?.(error);
957
1295
  }
958
1296
  }
@@ -969,8 +1307,8 @@ var _WikiMemory = class _WikiMemory {
969
1307
  const idChunk = topIds.slice(i, i + kwChunkSize);
970
1308
  const placeholders = idChunk.map(() => "?").join(",");
971
1309
  const chunkRows = await this.db.getAllAsync(
972
- `SELECT * FROM ${this.prefix}entries WHERE id IN (${placeholders}) AND deleted_at IS NULL`,
973
- idChunk
1310
+ `SELECT * FROM ${this.prefix}entries WHERE id IN (${placeholders}) AND entity_id = ? AND deleted_at IS NULL`,
1311
+ [...idChunk, entityId]
974
1312
  );
975
1313
  kwRows.push(...chunkRows);
976
1314
  }
@@ -1026,6 +1364,133 @@ var _WikiMemory = class _WikiMemory {
1026
1364
  });
1027
1365
  return { facts: parsedFacts, tasks, events: events.reverse() };
1028
1366
  }
1367
+ /**
1368
+ * Stable tie-break sort: score desc → access_count desc → updated_at desc → id asc.
1369
+ */
1370
+ _tieBreakSort(items) {
1371
+ items.sort((a, b) => this._compareScoredRows(a, b));
1372
+ }
1373
+ /**
1374
+ * Comparator for score + deterministic tie-break fields.
1375
+ * Negative return means "a ranks ahead of b" for descending score order.
1376
+ */
1377
+ _compareScoredRows(a, b) {
1378
+ const scoreDiff = b.score - a.score;
1379
+ if (scoreDiff !== 0) return scoreDiff;
1380
+ const accessCountDiff = (b.access_count ?? 0) - (a.access_count ?? 0);
1381
+ if (accessCountDiff !== 0) return accessCountDiff;
1382
+ const updatedAtDiff = (b.updated_at ?? 0) - (a.updated_at ?? 0);
1383
+ if (updatedAtDiff !== 0) return updatedAtDiff;
1384
+ return a.id.localeCompare(b.id);
1385
+ }
1386
+ /**
1387
+ * Strip potentially sensitive data from ranker errors before exposing to host callbacks.
1388
+ * Preserves error type for debugging but removes message/stack that may contain credentials.
1389
+ * Recursively sanitizes one level of .cause; deeper chains collapse to type only.
1390
+ */
1391
+ _sanitizeRankerError(err) {
1392
+ if (this.options.sanitizeRankerErrors === false) {
1393
+ return err instanceof Error ? err : new Error(String(err));
1394
+ }
1395
+ const typeName = err instanceof Error ? err.constructor?.name ?? "Error" : typeof err;
1396
+ const innerCause = err instanceof Error && err.cause !== void 0 ? new Error(`Caused by: ${err.cause?.constructor?.name ?? typeof err.cause}`) : void 0;
1397
+ const sanitized = new Error(
1398
+ `VectorRanker ${typeName} (message scrubbed for security)`,
1399
+ innerCause ? { cause: innerCause } : void 0
1400
+ );
1401
+ sanitized.name = typeName;
1402
+ return sanitized;
1403
+ }
1404
+ /**
1405
+ * Score candidate rows using in-process JS cosine similarity.
1406
+ * Applies hybrid blending (if weight set) and tie-break sorting before returning.
1407
+ */
1408
+ async _rankWithJsCosine(args) {
1409
+ const queryVec = args.queryVec instanceof Float32Array ? args.queryVec.slice() : Array.from(args.queryVec);
1410
+ const { entityId, candidateRows, weight, miniSearchScores, populateCache, limit } = args;
1411
+ let entityCache = this.vectorCache.get(entityId);
1412
+ const tooLarge = populateCache && candidateRows.length > _WikiMemory.MAX_VECTOR_CACHE_FACTS_PER_ENTITY;
1413
+ if (tooLarge && entityCache) {
1414
+ this.vectorCache.delete(entityId);
1415
+ entityCache = void 0;
1416
+ }
1417
+ const canCache = populateCache && !tooLarge;
1418
+ if (canCache && !entityCache) {
1419
+ entityCache = /* @__PURE__ */ new Map();
1420
+ }
1421
+ const scored = candidateRows.map((row) => {
1422
+ let vector = entityCache?.get(row.id) ?? parseEmbedding(row.embedding_blob, row.embedding);
1423
+ if (vector && canCache && entityCache && !entityCache.has(row.id)) {
1424
+ entityCache.set(row.id, vector);
1425
+ }
1426
+ let score = 0;
1427
+ if (vector && vector.length === queryVec.length) {
1428
+ const cosSim = cosineSimilarity(queryVec, vector);
1429
+ if (weight !== void 0) {
1430
+ const kwScore = miniSearchScores?.get(row.id) ?? 0;
1431
+ score = weight * Math.max(0, cosSim) + (1 - weight) * kwScore;
1432
+ } else {
1433
+ score = cosSim;
1434
+ }
1435
+ } else if (weight !== void 0 && weight < 1) {
1436
+ const kwScore = miniSearchScores?.get(row.id) ?? 0;
1437
+ score = (1 - weight) * kwScore;
1438
+ } else {
1439
+ score = -2;
1440
+ }
1441
+ return { id: row.id, score, updated_at: row.updated_at, access_count: row.access_count };
1442
+ });
1443
+ if (canCache && entityCache && entityCache.size > 0) {
1444
+ if (!this.vectorCache.has(entityId)) {
1445
+ if (this.vectorCache.size >= _WikiMemory.MAX_VECTOR_CACHE_ENTITIES) {
1446
+ const oldestKey = this.vectorCache.keys().next().value;
1447
+ if (oldestKey !== void 0) this.vectorCache.delete(oldestKey);
1448
+ }
1449
+ this.vectorCache.set(entityId, entityCache);
1450
+ }
1451
+ }
1452
+ this._tieBreakSort(scored);
1453
+ return scored.slice(0, limit);
1454
+ }
1455
+ /**
1456
+ * Delegate semantic ranking to the injected VectorRanker.
1457
+ * Caller should pass an oversampledLimit to preserve recall after re-ranking.
1458
+ * Returns scored results ready for hybrid blending and tie-break sorting.
1459
+ */
1460
+ async _rankWithVectorRanker(args) {
1461
+ const { entityId, candidateIds, weight, miniSearchScores, limit } = args;
1462
+ const ranker = this.options.vectorRanker;
1463
+ if (!ranker) {
1464
+ throw new Error("vectorRanker not configured");
1465
+ }
1466
+ const queryVecCopy = args.queryVec instanceof Float32Array ? args.queryVec.slice() : Array.from(args.queryVec);
1467
+ const rankerResults = await ranker.rankBySimilarity({
1468
+ entityId,
1469
+ queryVec: queryVecCopy,
1470
+ candidateIds,
1471
+ limit
1472
+ });
1473
+ const allowedIds = candidateIds ? new Set(candidateIds) : void 0;
1474
+ const seen = /* @__PURE__ */ new Set();
1475
+ const normalized = [];
1476
+ for (const r of rankerResults) {
1477
+ if (normalized.length >= limit) break;
1478
+ if (seen.has(r.id)) continue;
1479
+ if (allowedIds && !allowedIds.has(r.id)) continue;
1480
+ if (!Number.isFinite(r.semanticScore)) continue;
1481
+ seen.add(r.id);
1482
+ normalized.push(r);
1483
+ }
1484
+ const scored = normalized.map((r) => {
1485
+ let score = r.semanticScore;
1486
+ if (weight !== void 0) {
1487
+ const kwScore = miniSearchScores?.get(r.id) ?? 0;
1488
+ score = weight * Math.max(0, r.semanticScore) + (1 - weight) * kwScore;
1489
+ }
1490
+ return { id: r.id, score };
1491
+ });
1492
+ return scored;
1493
+ }
1029
1494
  async getMemoryBundle(entityId) {
1030
1495
  return this._getFullBundle(entityId, { maxEvents: 10 });
1031
1496
  }
@@ -1142,7 +1607,7 @@ ${JSON.stringify(currentFacts, null, 2)}`;
1142
1607
  INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, created_at, updated_at)
1143
1608
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1144
1609
  `, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "agent_inferred", now, now]);
1145
- insertedFacts.push({ id, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
1610
+ insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
1146
1611
  }
1147
1612
  for (const task of validTasks) {
1148
1613
  const id = generateId("task_");
@@ -1222,6 +1687,7 @@ The following document anchors are provided for contradiction detection only. Do
1222
1687
  const safeDeleted = deleted.filter((id) => mutableIds.has(id));
1223
1688
  const validNewFacts = newFacts.map(validateFact).filter((f) => f !== null);
1224
1689
  const insertedFacts = [];
1690
+ const uniqueDeletedFactIds = Array.from(new Set(safeDeleted));
1225
1691
  await this.db.withTransactionAsync(async () => {
1226
1692
  for (const id of safeDowngraded) {
1227
1693
  await this.db.runAsync(`UPDATE ${this.prefix}entries SET confidence = 'tentative', updated_at = ? WHERE id = ? AND entity_id = ?`, [now, id, entityId]);
@@ -1235,11 +1701,18 @@ The following document anchors are provided for contradiction detection only. Do
1235
1701
  INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, created_at, updated_at)
1236
1702
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1237
1703
  `, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "agent_inferred", now, now]);
1238
- insertedFacts.push({ id, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
1704
+ insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
1239
1705
  }
1240
1706
  });
1241
1707
  this.vectorCache.delete(entityId);
1242
1708
  await this.rebuildMiniSearchIndex(entityId);
1709
+ for (const factId of uniqueDeletedFactIds) {
1710
+ try {
1711
+ await this._notifyEmbeddingPersisted(entityId, factId, null);
1712
+ } catch (hookErr) {
1713
+ console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during heal for ${factId}:`, hookErr);
1714
+ }
1715
+ }
1243
1716
  for (const fact of insertedFacts) {
1244
1717
  await this.embedFact(fact);
1245
1718
  }
@@ -1539,10 +2012,17 @@ The following document anchors are provided for contradiction detection only. Do
1539
2012
  }
1540
2013
  async _doImportEntity(entityId, bundle, merge) {
1541
2014
  const upsertedFactIds = /* @__PURE__ */ new Set();
1542
- const factsWithPreservedBlob = /* @__PURE__ */ new Set();
2015
+ const upsertedDeletedFactIds = /* @__PURE__ */ new Set();
2016
+ const factsWithPreservedBlob = /* @__PURE__ */ new Map();
1543
2017
  const preservedBlobDims = /* @__PURE__ */ new Set();
2018
+ const softDeletedFactIds = [];
1544
2019
  await this.db.withTransactionAsync(async () => {
1545
2020
  if (!merge) {
2021
+ const toDelete = await this.db.getAllAsync(
2022
+ `SELECT id FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`,
2023
+ [entityId]
2024
+ );
2025
+ softDeletedFactIds.push(...toDelete.map((r) => r.id));
1546
2026
  const now = Date.now();
1547
2027
  await this.db.runAsync(
1548
2028
  `UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE entity_id = ? AND deleted_at IS NULL`,
@@ -1596,7 +2076,8 @@ The following document anchors are provided for contradiction detection only. Do
1596
2076
  let blobData = null;
1597
2077
  if (rawBlob !== null && rawBlob.byteLength > 0 && rawBlob.byteLength % 4 === 0) {
1598
2078
  const copy = new ArrayBuffer(rawBlob.byteLength);
1599
- new Uint8Array(copy).set(rawBlob);
2079
+ const alignedBlob = new Uint8Array(copy);
2080
+ alignedBlob.set(rawBlob);
1600
2081
  const floats = new Float32Array(copy, 0, rawBlob.byteLength / 4);
1601
2082
  let allFinite = true;
1602
2083
  for (let i = 0; i < floats.length; i++) {
@@ -1606,7 +2087,7 @@ The following document anchors are provided for contradiction detection only. Do
1606
2087
  }
1607
2088
  }
1608
2089
  if (allFinite) {
1609
- blobData = rawBlob;
2090
+ blobData = alignedBlob;
1610
2091
  }
1611
2092
  }
1612
2093
  if (existing) {
@@ -1622,7 +2103,7 @@ The following document anchors are provided for contradiction detection only. Do
1622
2103
  `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 = ?`,
1623
2104
  [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]
1624
2105
  );
1625
- factsWithPreservedBlob.add(fact.id);
2106
+ factsWithPreservedBlob.set(fact.id, blobData);
1626
2107
  if (!fact.deleted_at) preservedBlobDims.add(blobData.byteLength / 4);
1627
2108
  } else {
1628
2109
  await this.db.runAsync(
@@ -1632,13 +2113,14 @@ The following document anchors are provided for contradiction detection only. Do
1632
2113
  }
1633
2114
  existingFactsById.set(fact.id, { id: fact.id, entity_id: entityId, updated_at: safeUpdatedAt });
1634
2115
  upsertedFactIds.add(fact.id);
2116
+ if (fact.deleted_at) upsertedDeletedFactIds.add(fact.id);
1635
2117
  } else {
1636
2118
  if (blobData != null) {
1637
2119
  await this.db.runAsync(
1638
2120
  `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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1639
2121
  [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]
1640
2122
  );
1641
- factsWithPreservedBlob.add(fact.id);
2123
+ factsWithPreservedBlob.set(fact.id, blobData);
1642
2124
  if (!fact.deleted_at) preservedBlobDims.add(blobData.byteLength / 4);
1643
2125
  } else {
1644
2126
  await this.db.runAsync(
@@ -1648,6 +2130,7 @@ The following document anchors are provided for contradiction detection only. Do
1648
2130
  }
1649
2131
  existingFactsById.set(fact.id, { id: fact.id, entity_id: entityId, updated_at: safeUpdatedAt });
1650
2132
  upsertedFactIds.add(fact.id);
2133
+ if (fact.deleted_at) upsertedDeletedFactIds.add(fact.id);
1651
2134
  }
1652
2135
  }
1653
2136
  const taskIds = bundle.tasks.map((task) => task.id);
@@ -1703,12 +2186,34 @@ The following document anchors are provided for contradiction detection only. Do
1703
2186
  if (!fact.deleted_at && upsertedFactIds.has(fact.id) && !factsWithPreservedBlob.has(fact.id)) {
1704
2187
  await this.embedFact({
1705
2188
  id: fact.id,
2189
+ entity_id: entityId,
2190
+ // Use authoritative entityId from dump key, not fact.entity_id
1706
2191
  title: fact.title,
1707
2192
  body: fact.body,
1708
2193
  tags: Array.isArray(fact.tags) || typeof fact.tags === "string" ? fact.tags : []
1709
2194
  });
1710
2195
  }
1711
2196
  }
2197
+ for (const fact of bundle.facts) {
2198
+ const blobData = factsWithPreservedBlob.get(fact.id);
2199
+ if (blobData && !fact.deleted_at && upsertedFactIds.has(fact.id)) {
2200
+ try {
2201
+ const float32Vector = new Float32Array(blobData.buffer, blobData.byteOffset, blobData.byteLength / 4);
2202
+ await this._notifyEmbeddingPersisted(entityId, fact.id, float32Vector);
2203
+ } catch (hookErr) {
2204
+ console.warn(`[WikiMemory] onEmbeddingPersisted hook failed for preserved-blob fact ${fact.id}:`, hookErr);
2205
+ }
2206
+ }
2207
+ }
2208
+ for (const factId of softDeletedFactIds) {
2209
+ if (!upsertedFactIds.has(factId) || upsertedDeletedFactIds.has(factId)) {
2210
+ try {
2211
+ await this._notifyEmbeddingPersisted(entityId, factId, null);
2212
+ } catch (hookErr) {
2213
+ console.warn(`[WikiMemory] onEmbeddingPersisted(vector=null) hook failed for soft-deleted fact ${factId}:`, hookErr);
2214
+ }
2215
+ }
2216
+ }
1712
2217
  try {
1713
2218
  const canonicalRow = await this.db.getFirstAsync(
1714
2219
  `SELECT value FROM ${this.prefix}meta WHERE key = 'embedding_dimension'`
@@ -1779,7 +2284,17 @@ The following document anchors are provided for contradiction detection only. Do
1779
2284
  const now = Date.now();
1780
2285
  let deletedEntries = 0;
1781
2286
  let deletedTasks = 0;
2287
+ const deletedEntryIds = [];
1782
2288
  if (params.clearAll) {
2289
+ const newDeletions = await this.db.getAllAsync(
2290
+ `SELECT id FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`,
2291
+ [entityId]
2292
+ );
2293
+ const alreadySoftDeleted = await this.db.getAllAsync(
2294
+ `SELECT id FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NOT NULL`,
2295
+ [entityId]
2296
+ );
2297
+ deletedEntryIds.push(...newDeletions.map((e) => e.id), ...alreadySoftDeleted.map((e) => e.id));
1783
2298
  const [entriesRes, tasksRes] = await Promise.all([
1784
2299
  this.db.runAsync(`UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE entity_id = ? AND deleted_at IS NULL`, [now, now, entityId]),
1785
2300
  this.db.runAsync(`UPDATE ${this.prefix}tasks SET deleted_at = ?, updated_at = ? WHERE entity_id = ? AND deleted_at IS NULL`, [now, now, entityId])
@@ -1797,6 +2312,27 @@ The following document anchors are provided for contradiction detection only. Do
1797
2312
  if (params.sourceRef !== void 0 && !sourceRef) throw new Error("Invalid sourceRef");
1798
2313
  const sourceHash = params.sourceHash !== void 0 ? normalizeSourceHash(params.sourceHash) : null;
1799
2314
  if (params.sourceHash !== void 0 && !sourceHash) throw new Error("Invalid sourceHash (must be 64-char hex string)");
2315
+ if (params.entryId) {
2316
+ const entry = await this.db.getFirstAsync(
2317
+ `SELECT id FROM ${this.prefix}entries WHERE id = ? AND entity_id = ?`,
2318
+ [params.entryId, entityId]
2319
+ );
2320
+ if (entry) deletedEntryIds.push(entry.id);
2321
+ }
2322
+ if (sourceRef || sourceHash) {
2323
+ let q = `SELECT id FROM ${this.prefix}entries WHERE entity_id = ?`;
2324
+ const args = [entityId];
2325
+ if (sourceRef) {
2326
+ q += ` AND source_ref = ?`;
2327
+ args.push(sourceRef);
2328
+ }
2329
+ if (sourceHash) {
2330
+ q += ` AND source_hash = ?`;
2331
+ args.push(sourceHash);
2332
+ }
2333
+ const entriesToDelete = await this.db.getAllAsync(q, args);
2334
+ deletedEntryIds.push(...entriesToDelete.map((e) => e.id));
2335
+ }
1800
2336
  const entryPromise = params.entryId ? this.db.runAsync(`UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE id = ? AND entity_id = ? AND deleted_at IS NULL`, [now, now, params.entryId, entityId]) : null;
1801
2337
  const taskPromise = params.taskId ? this.db.runAsync(`UPDATE ${this.prefix}tasks SET deleted_at = ?, updated_at = ? WHERE id = ? AND entity_id = ? AND deleted_at IS NULL`, [now, now, params.taskId, entityId]) : null;
1802
2338
  let refPromise = null;
@@ -1824,6 +2360,31 @@ The following document anchors are provided for contradiction detection only. Do
1824
2360
  }
1825
2361
  await this.rebuildMiniSearchIndex(entityId);
1826
2362
  this.vectorCache.delete(entityId);
2363
+ const uniqueDeletedIds = Array.from(new Set(deletedEntryIds));
2364
+ for (const factId of uniqueDeletedIds) {
2365
+ try {
2366
+ await this._notifyEmbeddingPersistedOrThrow(entityId, factId, null);
2367
+ } catch (hookErr) {
2368
+ const isTimeout = hookErr?.[HOOK_TIMEOUT_MARKER] === true;
2369
+ if (isTimeout) {
2370
+ throw new Error(
2371
+ `forget(${entityId}/${factId}) failed: ${hookErr.message}`
2372
+ );
2373
+ }
2374
+ const errMsg = hookErr?.message ?? "";
2375
+ const isValidationError = errMsg.startsWith("Invalid deletionHookTimeoutMs");
2376
+ if (isValidationError) {
2377
+ throw new Error(
2378
+ `forget(${entityId}/${factId}) failed: ${errMsg}`,
2379
+ { cause: hookErr }
2380
+ );
2381
+ }
2382
+ throw new Error(
2383
+ `forget(${entityId}/${factId}) failed: ANN cleanup hook rejected`,
2384
+ { cause: this._sanitizeRankerError(hookErr) }
2385
+ );
2386
+ }
2387
+ }
1827
2388
  return { deleted: { entries: deletedEntries, tasks: deletedTasks } };
1828
2389
  } finally {
1829
2390
  this.activeMaintenanceJobs.delete(forgetKey);
@@ -1893,7 +2454,15 @@ ${chunk}`;
1893
2454
  }
1894
2455
  const now = Date.now();
1895
2456
  const insertedFacts = [];
2457
+ const deletedSourceFactIds = [];
1896
2458
  await this.db.withTransactionAsync(async () => {
2459
+ const existingSourceFacts = await this.db.getAllAsync(
2460
+ `SELECT id FROM ${this.prefix}entries WHERE source_ref = ? AND entity_id = ? AND deleted_at IS NULL`,
2461
+ [sourceRef, entityId]
2462
+ );
2463
+ for (const row of existingSourceFacts) {
2464
+ deletedSourceFactIds.push(row.id);
2465
+ }
1897
2466
  await this.db.runAsync(
1898
2467
  `UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE source_ref = ? AND entity_id = ? AND deleted_at IS NULL`,
1899
2468
  [now, now, sourceRef, entityId]
@@ -1905,11 +2474,19 @@ ${chunk}`;
1905
2474
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1906
2475
  [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "user_document", sourceHash, sourceRef, now, now]
1907
2476
  );
1908
- insertedFacts.push({ id, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
2477
+ insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
1909
2478
  }
1910
2479
  });
1911
2480
  await this.rebuildMiniSearchIndex(entityId);
1912
2481
  this.vectorCache.delete(entityId);
2482
+ const uniqueDeletedSourceFactIds = Array.from(new Set(deletedSourceFactIds));
2483
+ for (const factId of uniqueDeletedSourceFactIds) {
2484
+ try {
2485
+ await this._notifyEmbeddingPersisted(entityId, factId, null);
2486
+ } catch (hookErr) {
2487
+ console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during ingest for ${factId}:`, hookErr);
2488
+ }
2489
+ }
1913
2490
  for (const fact of insertedFacts) {
1914
2491
  await this.embedFact(fact);
1915
2492
  }
@@ -2154,6 +2731,6 @@ function createWiki(db, options) {
2154
2731
  return new WikiMemory(db, options);
2155
2732
  }
2156
2733
 
2157
- export { WikiBusyError, WikiMemory, createWiki, formatContext, formatMemoryDump };
2734
+ export { PrunePartialFailureError, WikiBusyError, WikiMemory, createWiki, formatContext, formatMemoryDump };
2158
2735
  //# sourceMappingURL=index.mjs.map
2159
2736
  //# sourceMappingURL=index.mjs.map