@equationalapplications/core-llm-wiki 3.0.0 → 3.1.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/README.md +159 -1
- package/dist/index.d.mts +108 -7
- package/dist/index.d.ts +108 -7
- package/dist/index.js +518 -83
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +518 -83
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -560,6 +560,11 @@ var _WikiMemory = class _WikiMemory {
|
|
|
560
560
|
`UPDATE ${this.prefix}entries SET embedding_blob = ?, embedding = NULL WHERE id = ?`,
|
|
561
561
|
[blob, fact.id]
|
|
562
562
|
);
|
|
563
|
+
try {
|
|
564
|
+
await this._notifyEmbeddingPersisted(fact.entity_id, fact.id, float32Vector);
|
|
565
|
+
} catch (hookErr) {
|
|
566
|
+
console.warn(`[WikiMemory] onEmbeddingPersisted hook failed for ${fact.id}:`, hookErr);
|
|
567
|
+
}
|
|
563
568
|
return true;
|
|
564
569
|
} catch (err) {
|
|
565
570
|
console.warn(`[WikiMemory] embedFact failed for ${fact.id}:`, err);
|
|
@@ -575,6 +580,9 @@ var _WikiMemory = class _WikiMemory {
|
|
|
575
580
|
_warnCrossEntityCollision(type, id, existingEntityId, targetEntityId) {
|
|
576
581
|
console.warn(`[WikiMemory] importDump: ${type} id "${id}" already belongs to entity "${existingEntityId}"; skipping for entity "${targetEntityId}"`);
|
|
577
582
|
}
|
|
583
|
+
async _notifyEmbeddingPersisted(entityId, factId, vector) {
|
|
584
|
+
await this.options.vectorRanker?.onEmbeddingPersisted?.({ entityId, factId, vector });
|
|
585
|
+
}
|
|
578
586
|
async setup() {
|
|
579
587
|
const entriesExistedBeforeSetup = await this.db.getFirstAsync(
|
|
580
588
|
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
|
|
@@ -757,8 +765,15 @@ var _WikiMemory = class _WikiMemory {
|
|
|
757
765
|
let deletedEntries = 0;
|
|
758
766
|
let deletedTasks = 0;
|
|
759
767
|
let deletedEvents = 0;
|
|
768
|
+
const deletedEntryIds = [];
|
|
760
769
|
if (retainSoftDeletedFor !== null) {
|
|
761
770
|
const cutoff = now - retainSoftDeletedFor * 864e5;
|
|
771
|
+
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 < ?`,
|
|
774
|
+
[entityId, cutoff]
|
|
775
|
+
);
|
|
776
|
+
deletedEntryIds.push(...entriesToDelete.map((e) => e.id));
|
|
762
777
|
const entryResult = await this.db.runAsync(
|
|
763
778
|
`DELETE FROM ${this.prefix}entries
|
|
764
779
|
WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at < ?`,
|
|
@@ -787,6 +802,14 @@ var _WikiMemory = class _WikiMemory {
|
|
|
787
802
|
}
|
|
788
803
|
await this.rebuildMiniSearchIndex(entityId);
|
|
789
804
|
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
|
+
}
|
|
790
813
|
return { entries: deletedEntries, tasks: deletedTasks, events: deletedEvents };
|
|
791
814
|
} finally {
|
|
792
815
|
this.activeMaintenanceJobs.delete(pruneKey);
|
|
@@ -807,6 +830,10 @@ var _WikiMemory = class _WikiMemory {
|
|
|
807
830
|
if (maxResults === 0) ; else if (trimmedQuery) {
|
|
808
831
|
let usedEmbed = false;
|
|
809
832
|
if (!skipEmbed && embedFn) {
|
|
833
|
+
let rankerShouldRethrow = false;
|
|
834
|
+
let pendingRankerFallbackError;
|
|
835
|
+
let usedKeywordFallback = false;
|
|
836
|
+
let scoredAlreadySortedAndLimited = false;
|
|
810
837
|
try {
|
|
811
838
|
const queryVec = await embedFn(trimmedQuery);
|
|
812
839
|
if (queryVec.length === 0 || !queryVec.every((v) => typeof v === "number" && isFinite(v))) {
|
|
@@ -838,6 +865,7 @@ var _WikiMemory = class _WikiMemory {
|
|
|
838
865
|
`Some facts have embeddings that do not match the current model dimension. Call runReembed() to rebuild all embeddings consistently.`
|
|
839
866
|
);
|
|
840
867
|
}
|
|
868
|
+
const useRanker = Boolean(this.options.vectorRanker);
|
|
841
869
|
let candidateRows;
|
|
842
870
|
let populateCache = true;
|
|
843
871
|
let miniSearchScores;
|
|
@@ -856,15 +884,30 @@ var _WikiMemory = class _WikiMemory {
|
|
|
856
884
|
} else {
|
|
857
885
|
const topKIds = topKResults.map((r) => r.id);
|
|
858
886
|
const inClauseChunkSize = 500;
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
887
|
+
if (useRanker) {
|
|
888
|
+
const rows = [];
|
|
889
|
+
for (let i = 0; i < topKIds.length; i += inClauseChunkSize) {
|
|
890
|
+
const idChunk = topKIds.slice(i, i + inClauseChunkSize);
|
|
891
|
+
const placeholders = idChunk.map(() => "?").join(",");
|
|
892
|
+
const chunkRows = await this.db.getAllAsync(
|
|
893
|
+
`SELECT id, updated_at, access_count FROM ${this.prefix}entries WHERE id IN (${placeholders}) AND deleted_at IS NULL`,
|
|
894
|
+
idChunk
|
|
895
|
+
);
|
|
896
|
+
rows.push(...chunkRows);
|
|
897
|
+
}
|
|
898
|
+
candidateRows = rows;
|
|
899
|
+
} else {
|
|
900
|
+
const rows = [];
|
|
901
|
+
for (let i = 0; i < topKIds.length; i += inClauseChunkSize) {
|
|
902
|
+
const idChunk = topKIds.slice(i, i + inClauseChunkSize);
|
|
903
|
+
const placeholders = idChunk.map(() => "?").join(",");
|
|
904
|
+
const chunkRows = await this.db.getAllAsync(
|
|
905
|
+
`SELECT id, embedding_blob, embedding, updated_at, access_count FROM ${this.prefix}entries WHERE id IN (${placeholders}) AND deleted_at IS NULL`,
|
|
906
|
+
idChunk
|
|
907
|
+
);
|
|
908
|
+
rows.push(...chunkRows);
|
|
909
|
+
}
|
|
910
|
+
candidateRows = rows;
|
|
868
911
|
}
|
|
869
912
|
if (weight !== void 0 && weight < 1) {
|
|
870
913
|
const maxMsScore = Math.max(1, topKResults[0]?.score ?? 1);
|
|
@@ -873,10 +916,17 @@ var _WikiMemory = class _WikiMemory {
|
|
|
873
916
|
}
|
|
874
917
|
}
|
|
875
918
|
} else {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
919
|
+
if (useRanker) {
|
|
920
|
+
candidateRows = await this.db.getAllAsync(
|
|
921
|
+
`SELECT id, updated_at, access_count FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`,
|
|
922
|
+
[entityId]
|
|
923
|
+
);
|
|
924
|
+
} else {
|
|
925
|
+
candidateRows = await this.db.getAllAsync(
|
|
926
|
+
`SELECT id, embedding_blob, embedding, updated_at, access_count FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`,
|
|
927
|
+
[entityId]
|
|
928
|
+
);
|
|
929
|
+
}
|
|
880
930
|
if (weight !== void 0 && weight < 1) {
|
|
881
931
|
const msResults = this.miniSearch.search(trimmedQuery, {
|
|
882
932
|
filter: (r) => r.entity_id === entityId,
|
|
@@ -889,76 +939,263 @@ var _WikiMemory = class _WikiMemory {
|
|
|
889
939
|
if (candidateRows === null) {
|
|
890
940
|
usedEmbed = true;
|
|
891
941
|
} else {
|
|
892
|
-
let
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
942
|
+
let scored;
|
|
943
|
+
if (useRanker) {
|
|
944
|
+
const candidateIds = effectivePreFilterLimit !== void 0 ? candidateRows.map((r) => r.id) : void 0;
|
|
945
|
+
try {
|
|
946
|
+
const oversampledLimit = Math.max(maxResults * 2, maxResults + 50);
|
|
947
|
+
scored = await this._rankWithVectorRanker({
|
|
948
|
+
entityId,
|
|
949
|
+
queryVec,
|
|
950
|
+
candidateIds,
|
|
951
|
+
weight,
|
|
952
|
+
miniSearchScores,
|
|
953
|
+
limit: oversampledLimit
|
|
954
|
+
});
|
|
955
|
+
if (scored.length > 0) {
|
|
956
|
+
const scoredIds2 = new Set(scored.map((s) => s.id));
|
|
957
|
+
const metaMap = /* @__PURE__ */ new Map();
|
|
958
|
+
for (const r of candidateRows) {
|
|
959
|
+
if (scoredIds2.has(r.id)) {
|
|
960
|
+
metaMap.set(r.id, { updated_at: r.updated_at, access_count: r.access_count });
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
scored = scored.map((s) => {
|
|
964
|
+
const meta = metaMap.get(s.id);
|
|
965
|
+
return { ...s, updated_at: meta?.updated_at ?? null, access_count: meta?.access_count ?? null };
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
const scoredIds = new Set(scored.map((s) => s.id));
|
|
969
|
+
const isHybrid = weight !== void 0 && weight < 1;
|
|
970
|
+
const maxBackfill = isHybrid ? maxResults : Math.max(0, maxResults - scored.length);
|
|
971
|
+
if (maxBackfill > 0) {
|
|
972
|
+
if (isHybrid) {
|
|
973
|
+
const topK = [];
|
|
974
|
+
for (const row of candidateRows) {
|
|
975
|
+
if (scoredIds.has(row.id)) continue;
|
|
976
|
+
const kwScore = miniSearchScores?.get(row.id) ?? 0;
|
|
977
|
+
const candidate = { row, kwScore };
|
|
978
|
+
if (topK.length < maxBackfill) {
|
|
979
|
+
let insertIdx = topK.length;
|
|
980
|
+
for (let i = 0; i < topK.length; i++) {
|
|
981
|
+
const cmp = this._compareScoredRows(
|
|
982
|
+
{
|
|
983
|
+
id: candidate.row.id,
|
|
984
|
+
score: candidate.kwScore,
|
|
985
|
+
updated_at: candidate.row.updated_at,
|
|
986
|
+
access_count: candidate.row.access_count
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
id: topK[i].row.id,
|
|
990
|
+
score: topK[i].kwScore,
|
|
991
|
+
updated_at: topK[i].row.updated_at,
|
|
992
|
+
access_count: topK[i].row.access_count
|
|
993
|
+
}
|
|
994
|
+
);
|
|
995
|
+
if (cmp < 0) {
|
|
996
|
+
insertIdx = i;
|
|
997
|
+
break;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
topK.splice(insertIdx, 0, candidate);
|
|
1001
|
+
} else {
|
|
1002
|
+
const cmpWorst = this._compareScoredRows(
|
|
1003
|
+
{
|
|
1004
|
+
id: candidate.row.id,
|
|
1005
|
+
score: candidate.kwScore,
|
|
1006
|
+
updated_at: candidate.row.updated_at,
|
|
1007
|
+
access_count: candidate.row.access_count
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
id: topK[maxBackfill - 1].row.id,
|
|
1011
|
+
score: topK[maxBackfill - 1].kwScore,
|
|
1012
|
+
updated_at: topK[maxBackfill - 1].row.updated_at,
|
|
1013
|
+
access_count: topK[maxBackfill - 1].row.access_count
|
|
1014
|
+
}
|
|
1015
|
+
);
|
|
1016
|
+
if (cmpWorst < 0) {
|
|
1017
|
+
let insertIdx = maxBackfill - 1;
|
|
1018
|
+
for (let i = 0; i < topK.length; i++) {
|
|
1019
|
+
const cmp = this._compareScoredRows(
|
|
1020
|
+
{
|
|
1021
|
+
id: candidate.row.id,
|
|
1022
|
+
score: candidate.kwScore,
|
|
1023
|
+
updated_at: candidate.row.updated_at,
|
|
1024
|
+
access_count: candidate.row.access_count
|
|
1025
|
+
},
|
|
1026
|
+
{
|
|
1027
|
+
id: topK[i].row.id,
|
|
1028
|
+
score: topK[i].kwScore,
|
|
1029
|
+
updated_at: topK[i].row.updated_at,
|
|
1030
|
+
access_count: topK[i].row.access_count
|
|
1031
|
+
}
|
|
1032
|
+
);
|
|
1033
|
+
if (cmp < 0) {
|
|
1034
|
+
insertIdx = i;
|
|
1035
|
+
break;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
topK.splice(insertIdx, 0, candidate);
|
|
1039
|
+
topK.pop();
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
for (const { row, kwScore } of topK) {
|
|
1044
|
+
scored.push({
|
|
1045
|
+
id: row.id,
|
|
1046
|
+
score: (1 - weight) * kwScore,
|
|
1047
|
+
updated_at: row.updated_at,
|
|
1048
|
+
access_count: row.access_count
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
} else {
|
|
1052
|
+
const omitted = [];
|
|
1053
|
+
for (const row of candidateRows) {
|
|
1054
|
+
if (scoredIds.has(row.id)) continue;
|
|
1055
|
+
omitted.push({ id: row.id, score: -2, updated_at: row.updated_at, access_count: row.access_count });
|
|
1056
|
+
}
|
|
1057
|
+
if (omitted.length > 0) {
|
|
1058
|
+
this._tieBreakSort(omitted);
|
|
1059
|
+
scored.push(...omitted.slice(0, maxBackfill));
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
} catch (rankerErr) {
|
|
1064
|
+
const rankerError = rankerErr instanceof Error ? rankerErr : new Error(String(rankerErr));
|
|
1065
|
+
const policy = this.options.vectorRankerFallback ?? "js-cosine";
|
|
1066
|
+
this.options.onVectorRankerFallback?.({ error: rankerError, policy });
|
|
1067
|
+
if (policy === "throw") {
|
|
1068
|
+
rankerShouldRethrow = true;
|
|
1069
|
+
throw rankerError;
|
|
1070
|
+
} else if (policy === "js-cosine") {
|
|
1071
|
+
let fallbackRows = candidateRows;
|
|
1072
|
+
if (fallbackRows && fallbackRows.length > 0 && !("embedding_blob" in fallbackRows[0])) {
|
|
1073
|
+
const rowIds = fallbackRows.map((r) => r.id);
|
|
1074
|
+
const embeddingsMap = /* @__PURE__ */ new Map();
|
|
1075
|
+
const chunkSize = 500;
|
|
1076
|
+
for (let i = 0; i < rowIds.length; i += chunkSize) {
|
|
1077
|
+
const idChunk = rowIds.slice(i, i + chunkSize);
|
|
1078
|
+
const placeholders = idChunk.map(() => "?").join(",");
|
|
1079
|
+
const embeddingRows = await this.db.getAllAsync(
|
|
1080
|
+
`SELECT id, embedding_blob, embedding FROM ${this.prefix}entries WHERE id IN (${placeholders}) AND entity_id = ? AND deleted_at IS NULL`,
|
|
1081
|
+
[...idChunk, entityId]
|
|
1082
|
+
);
|
|
1083
|
+
for (const row of embeddingRows) {
|
|
1084
|
+
embeddingsMap.set(row.id, { embedding_blob: row.embedding_blob, embedding: row.embedding });
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
fallbackRows = fallbackRows.map((r) => ({
|
|
1088
|
+
...r,
|
|
1089
|
+
embedding_blob: embeddingsMap.get(r.id)?.embedding_blob ?? null,
|
|
1090
|
+
embedding: embeddingsMap.get(r.id)?.embedding ?? null
|
|
1091
|
+
}));
|
|
1092
|
+
}
|
|
1093
|
+
scored = await this._rankWithJsCosine({
|
|
1094
|
+
entityId,
|
|
1095
|
+
queryVec,
|
|
1096
|
+
candidateRows: fallbackRows,
|
|
1097
|
+
weight,
|
|
1098
|
+
miniSearchScores,
|
|
1099
|
+
populateCache,
|
|
1100
|
+
limit: maxResults
|
|
1101
|
+
});
|
|
1102
|
+
scoredAlreadySortedAndLimited = true;
|
|
1103
|
+
} else if (policy === "keyword") {
|
|
1104
|
+
const msResults = this.miniSearch.search(trimmedQuery, {
|
|
1105
|
+
filter: (r) => r.entity_id === entityId,
|
|
1106
|
+
combineWith: "OR"
|
|
1107
|
+
});
|
|
1108
|
+
const topResults = msResults.slice(0, maxResults);
|
|
1109
|
+
const resultIds = new Set(topResults.map((r) => r.id));
|
|
1110
|
+
const candidateMap = /* @__PURE__ */ new Map();
|
|
1111
|
+
for (const r of candidateRows) {
|
|
1112
|
+
if (resultIds.has(r.id)) {
|
|
1113
|
+
candidateMap.set(r.id, { updated_at: r.updated_at, access_count: r.access_count });
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
scored = topResults.map((r) => {
|
|
1117
|
+
const meta = candidateMap.get(r.id);
|
|
1118
|
+
return {
|
|
1119
|
+
id: r.id,
|
|
1120
|
+
score: r.score ?? 0,
|
|
1121
|
+
access_count: meta?.access_count ?? null,
|
|
1122
|
+
updated_at: meta?.updated_at ?? null
|
|
1123
|
+
};
|
|
1124
|
+
});
|
|
1125
|
+
usedKeywordFallback = true;
|
|
913
1126
|
} else {
|
|
914
|
-
|
|
1127
|
+
scored = [];
|
|
915
1128
|
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
score = -2;
|
|
921
|
-
}
|
|
922
|
-
return { row, score };
|
|
923
|
-
});
|
|
924
|
-
if (canCache && entityCache && entityCache.size > 0) {
|
|
925
|
-
if (!this.vectorCache.has(entityId)) {
|
|
926
|
-
if (this.vectorCache.size >= _WikiMemory.MAX_VECTOR_CACHE_ENTITIES) {
|
|
927
|
-
const oldestKey = this.vectorCache.keys().next().value;
|
|
928
|
-
if (oldestKey !== void 0) this.vectorCache.delete(oldestKey);
|
|
1129
|
+
if (this.options.propagateRankerFailureToRetrievalFallback) {
|
|
1130
|
+
const mirrored = new Error("Vector ranker failed, falling back");
|
|
1131
|
+
mirrored.cause = rankerError;
|
|
1132
|
+
pendingRankerFallbackError = mirrored;
|
|
929
1133
|
}
|
|
930
|
-
this.vectorCache.set(entityId, entityCache);
|
|
931
1134
|
}
|
|
1135
|
+
} else {
|
|
1136
|
+
scored = await this._rankWithJsCosine({
|
|
1137
|
+
entityId,
|
|
1138
|
+
queryVec,
|
|
1139
|
+
candidateRows,
|
|
1140
|
+
weight,
|
|
1141
|
+
miniSearchScores,
|
|
1142
|
+
populateCache,
|
|
1143
|
+
limit: maxResults
|
|
1144
|
+
});
|
|
1145
|
+
scoredAlreadySortedAndLimited = true;
|
|
932
1146
|
}
|
|
933
|
-
scored.
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
)
|
|
953
|
-
|
|
1147
|
+
if (scored.length > 0) {
|
|
1148
|
+
if (!usedKeywordFallback && !scoredAlreadySortedAndLimited) {
|
|
1149
|
+
this._tieBreakSort(scored);
|
|
1150
|
+
}
|
|
1151
|
+
const topIds = (scoredAlreadySortedAndLimited ? scored : scored.slice(0, maxResults)).map((s) => s.id);
|
|
1152
|
+
if (topIds.length > 0) {
|
|
1153
|
+
const fullRows = [];
|
|
1154
|
+
const phase2ChunkSize = 500;
|
|
1155
|
+
for (let i = 0; i < topIds.length; i += phase2ChunkSize) {
|
|
1156
|
+
const idChunk = topIds.slice(i, i + phase2ChunkSize);
|
|
1157
|
+
const placeholders = idChunk.map(() => "?").join(",");
|
|
1158
|
+
const chunkRows = await this.db.getAllAsync(
|
|
1159
|
+
`SELECT * FROM ${this.prefix}entries WHERE id IN (${placeholders}) AND entity_id = ? AND deleted_at IS NULL`,
|
|
1160
|
+
[...idChunk, entityId]
|
|
1161
|
+
);
|
|
1162
|
+
fullRows.push(...chunkRows);
|
|
1163
|
+
}
|
|
1164
|
+
const byId = new Map(fullRows.map((r) => [r.id, r]));
|
|
1165
|
+
facts = topIds.map((id) => byId.get(id)).filter((f) => f !== void 0);
|
|
1166
|
+
if (facts.length < topIds.length) {
|
|
1167
|
+
const missingIds = topIds.filter((id) => !byId.has(id));
|
|
1168
|
+
const missingCount = missingIds.length;
|
|
1169
|
+
const sample = missingIds.slice(0, 5);
|
|
1170
|
+
const sampleSuffix = sample.length > 0 ? ` Missing ID sample: ${sample.join(", ")}${missingIds.length > sample.length ? ", ..." : ""}.` : "";
|
|
1171
|
+
const error = new Error(
|
|
1172
|
+
`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
|
|
1173
|
+
);
|
|
1174
|
+
this.options.onRetrievalFallback?.(error);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
if (pendingRankerFallbackError) {
|
|
1178
|
+
this.options.onRetrievalFallback?.(pendingRankerFallbackError);
|
|
1179
|
+
pendingRankerFallbackError = void 0;
|
|
1180
|
+
}
|
|
1181
|
+
usedEmbed = true;
|
|
1182
|
+
} else {
|
|
1183
|
+
if (pendingRankerFallbackError) {
|
|
1184
|
+
this.options.onRetrievalFallback?.(pendingRankerFallbackError);
|
|
1185
|
+
pendingRankerFallbackError = void 0;
|
|
954
1186
|
}
|
|
955
|
-
|
|
956
|
-
facts = topIds.map((id) => byId.get(id)).filter((f) => f !== void 0);
|
|
1187
|
+
usedEmbed = true;
|
|
957
1188
|
}
|
|
958
|
-
usedEmbed = true;
|
|
959
1189
|
}
|
|
960
1190
|
} catch (err) {
|
|
961
1191
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
1192
|
+
if (rankerShouldRethrow) {
|
|
1193
|
+
throw error;
|
|
1194
|
+
}
|
|
1195
|
+
if (pendingRankerFallbackError) {
|
|
1196
|
+
error.cause = pendingRankerFallbackError;
|
|
1197
|
+
pendingRankerFallbackError = void 0;
|
|
1198
|
+
}
|
|
962
1199
|
this.options.onRetrievalFallback?.(error);
|
|
963
1200
|
}
|
|
964
1201
|
}
|
|
@@ -975,8 +1212,8 @@ var _WikiMemory = class _WikiMemory {
|
|
|
975
1212
|
const idChunk = topIds.slice(i, i + kwChunkSize);
|
|
976
1213
|
const placeholders = idChunk.map(() => "?").join(",");
|
|
977
1214
|
const chunkRows = await this.db.getAllAsync(
|
|
978
|
-
`SELECT * FROM ${this.prefix}entries WHERE id IN (${placeholders}) AND deleted_at IS NULL`,
|
|
979
|
-
idChunk
|
|
1215
|
+
`SELECT * FROM ${this.prefix}entries WHERE id IN (${placeholders}) AND entity_id = ? AND deleted_at IS NULL`,
|
|
1216
|
+
[...idChunk, entityId]
|
|
980
1217
|
);
|
|
981
1218
|
kwRows.push(...chunkRows);
|
|
982
1219
|
}
|
|
@@ -1032,6 +1269,113 @@ var _WikiMemory = class _WikiMemory {
|
|
|
1032
1269
|
});
|
|
1033
1270
|
return { facts: parsedFacts, tasks, events: events.reverse() };
|
|
1034
1271
|
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Stable tie-break sort: score desc → access_count desc → updated_at desc → id asc.
|
|
1274
|
+
*/
|
|
1275
|
+
_tieBreakSort(items) {
|
|
1276
|
+
items.sort((a, b) => this._compareScoredRows(a, b));
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Comparator for score + deterministic tie-break fields.
|
|
1280
|
+
* Negative return means "a ranks ahead of b" for descending score order.
|
|
1281
|
+
*/
|
|
1282
|
+
_compareScoredRows(a, b) {
|
|
1283
|
+
const scoreDiff = b.score - a.score;
|
|
1284
|
+
if (scoreDiff !== 0) return scoreDiff;
|
|
1285
|
+
const accessCountDiff = (b.access_count ?? 0) - (a.access_count ?? 0);
|
|
1286
|
+
if (accessCountDiff !== 0) return accessCountDiff;
|
|
1287
|
+
const updatedAtDiff = (b.updated_at ?? 0) - (a.updated_at ?? 0);
|
|
1288
|
+
if (updatedAtDiff !== 0) return updatedAtDiff;
|
|
1289
|
+
return a.id.localeCompare(b.id);
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Score candidate rows using in-process JS cosine similarity.
|
|
1293
|
+
* Applies hybrid blending (if weight set) and tie-break sorting before returning.
|
|
1294
|
+
*/
|
|
1295
|
+
async _rankWithJsCosine(args) {
|
|
1296
|
+
const { entityId, queryVec, candidateRows, weight, miniSearchScores, populateCache, limit } = args;
|
|
1297
|
+
let entityCache = this.vectorCache.get(entityId);
|
|
1298
|
+
const tooLarge = populateCache && candidateRows.length > _WikiMemory.MAX_VECTOR_CACHE_FACTS_PER_ENTITY;
|
|
1299
|
+
if (tooLarge && entityCache) {
|
|
1300
|
+
this.vectorCache.delete(entityId);
|
|
1301
|
+
entityCache = void 0;
|
|
1302
|
+
}
|
|
1303
|
+
const canCache = populateCache && !tooLarge;
|
|
1304
|
+
if (canCache && !entityCache) {
|
|
1305
|
+
entityCache = /* @__PURE__ */ new Map();
|
|
1306
|
+
}
|
|
1307
|
+
const scored = candidateRows.map((row) => {
|
|
1308
|
+
let vector = entityCache?.get(row.id) ?? parseEmbedding(row.embedding_blob, row.embedding);
|
|
1309
|
+
if (vector && canCache && entityCache && !entityCache.has(row.id)) {
|
|
1310
|
+
entityCache.set(row.id, vector);
|
|
1311
|
+
}
|
|
1312
|
+
let score = 0;
|
|
1313
|
+
if (vector && vector.length === queryVec.length) {
|
|
1314
|
+
const cosSim = cosineSimilarity(queryVec, vector);
|
|
1315
|
+
if (weight !== void 0) {
|
|
1316
|
+
const kwScore = miniSearchScores?.get(row.id) ?? 0;
|
|
1317
|
+
score = weight * Math.max(0, cosSim) + (1 - weight) * kwScore;
|
|
1318
|
+
} else {
|
|
1319
|
+
score = cosSim;
|
|
1320
|
+
}
|
|
1321
|
+
} else if (weight !== void 0 && weight < 1) {
|
|
1322
|
+
const kwScore = miniSearchScores?.get(row.id) ?? 0;
|
|
1323
|
+
score = (1 - weight) * kwScore;
|
|
1324
|
+
} else {
|
|
1325
|
+
score = -2;
|
|
1326
|
+
}
|
|
1327
|
+
return { id: row.id, score, updated_at: row.updated_at, access_count: row.access_count };
|
|
1328
|
+
});
|
|
1329
|
+
if (canCache && entityCache && entityCache.size > 0) {
|
|
1330
|
+
if (!this.vectorCache.has(entityId)) {
|
|
1331
|
+
if (this.vectorCache.size >= _WikiMemory.MAX_VECTOR_CACHE_ENTITIES) {
|
|
1332
|
+
const oldestKey = this.vectorCache.keys().next().value;
|
|
1333
|
+
if (oldestKey !== void 0) this.vectorCache.delete(oldestKey);
|
|
1334
|
+
}
|
|
1335
|
+
this.vectorCache.set(entityId, entityCache);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
this._tieBreakSort(scored);
|
|
1339
|
+
return scored.slice(0, limit);
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Delegate semantic ranking to the injected VectorRanker.
|
|
1343
|
+
* Caller should pass an oversampledLimit to preserve recall after re-ranking.
|
|
1344
|
+
* Returns scored results ready for hybrid blending and tie-break sorting.
|
|
1345
|
+
*/
|
|
1346
|
+
async _rankWithVectorRanker(args) {
|
|
1347
|
+
const { entityId, queryVec, candidateIds, weight, miniSearchScores, limit } = args;
|
|
1348
|
+
const ranker = this.options.vectorRanker;
|
|
1349
|
+
if (!ranker) {
|
|
1350
|
+
throw new Error("vectorRanker not configured");
|
|
1351
|
+
}
|
|
1352
|
+
const rankerResults = await ranker.rankBySimilarity({
|
|
1353
|
+
entityId,
|
|
1354
|
+
queryVec,
|
|
1355
|
+
candidateIds,
|
|
1356
|
+
limit
|
|
1357
|
+
});
|
|
1358
|
+
const allowedIds = candidateIds ? new Set(candidateIds) : void 0;
|
|
1359
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1360
|
+
const normalized = [];
|
|
1361
|
+
for (const r of rankerResults) {
|
|
1362
|
+
if (normalized.length >= limit) break;
|
|
1363
|
+
if (seen.has(r.id)) continue;
|
|
1364
|
+
if (allowedIds && !allowedIds.has(r.id)) continue;
|
|
1365
|
+
if (!Number.isFinite(r.semanticScore)) continue;
|
|
1366
|
+
seen.add(r.id);
|
|
1367
|
+
normalized.push(r);
|
|
1368
|
+
}
|
|
1369
|
+
const scored = normalized.map((r) => {
|
|
1370
|
+
let score = r.semanticScore;
|
|
1371
|
+
if (weight !== void 0) {
|
|
1372
|
+
const kwScore = miniSearchScores?.get(r.id) ?? 0;
|
|
1373
|
+
score = weight * Math.max(0, r.semanticScore) + (1 - weight) * kwScore;
|
|
1374
|
+
}
|
|
1375
|
+
return { id: r.id, score };
|
|
1376
|
+
});
|
|
1377
|
+
return scored;
|
|
1378
|
+
}
|
|
1035
1379
|
async getMemoryBundle(entityId) {
|
|
1036
1380
|
return this._getFullBundle(entityId, { maxEvents: 10 });
|
|
1037
1381
|
}
|
|
@@ -1148,7 +1492,7 @@ ${JSON.stringify(currentFacts, null, 2)}`;
|
|
|
1148
1492
|
INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, created_at, updated_at)
|
|
1149
1493
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1150
1494
|
`, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "agent_inferred", now, now]);
|
|
1151
|
-
insertedFacts.push({ id, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
1495
|
+
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
1152
1496
|
}
|
|
1153
1497
|
for (const task of validTasks) {
|
|
1154
1498
|
const id = generateId("task_");
|
|
@@ -1228,6 +1572,7 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1228
1572
|
const safeDeleted = deleted.filter((id) => mutableIds.has(id));
|
|
1229
1573
|
const validNewFacts = newFacts.map(validateFact).filter((f) => f !== null);
|
|
1230
1574
|
const insertedFacts = [];
|
|
1575
|
+
const uniqueDeletedFactIds = Array.from(new Set(safeDeleted));
|
|
1231
1576
|
await this.db.withTransactionAsync(async () => {
|
|
1232
1577
|
for (const id of safeDowngraded) {
|
|
1233
1578
|
await this.db.runAsync(`UPDATE ${this.prefix}entries SET confidence = 'tentative', updated_at = ? WHERE id = ? AND entity_id = ?`, [now, id, entityId]);
|
|
@@ -1241,11 +1586,18 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1241
1586
|
INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, created_at, updated_at)
|
|
1242
1587
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1243
1588
|
`, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "agent_inferred", now, now]);
|
|
1244
|
-
insertedFacts.push({ id, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
1589
|
+
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
1245
1590
|
}
|
|
1246
1591
|
});
|
|
1247
1592
|
this.vectorCache.delete(entityId);
|
|
1248
1593
|
await this.rebuildMiniSearchIndex(entityId);
|
|
1594
|
+
for (const factId of uniqueDeletedFactIds) {
|
|
1595
|
+
try {
|
|
1596
|
+
await this._notifyEmbeddingPersisted(entityId, factId, null);
|
|
1597
|
+
} catch (hookErr) {
|
|
1598
|
+
console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during heal for ${factId}:`, hookErr);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1249
1601
|
for (const fact of insertedFacts) {
|
|
1250
1602
|
await this.embedFact(fact);
|
|
1251
1603
|
}
|
|
@@ -1545,10 +1897,17 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1545
1897
|
}
|
|
1546
1898
|
async _doImportEntity(entityId, bundle, merge) {
|
|
1547
1899
|
const upsertedFactIds = /* @__PURE__ */ new Set();
|
|
1548
|
-
const
|
|
1900
|
+
const upsertedDeletedFactIds = /* @__PURE__ */ new Set();
|
|
1901
|
+
const factsWithPreservedBlob = /* @__PURE__ */ new Map();
|
|
1549
1902
|
const preservedBlobDims = /* @__PURE__ */ new Set();
|
|
1903
|
+
const softDeletedFactIds = [];
|
|
1550
1904
|
await this.db.withTransactionAsync(async () => {
|
|
1551
1905
|
if (!merge) {
|
|
1906
|
+
const toDelete = await this.db.getAllAsync(
|
|
1907
|
+
`SELECT id FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`,
|
|
1908
|
+
[entityId]
|
|
1909
|
+
);
|
|
1910
|
+
softDeletedFactIds.push(...toDelete.map((r) => r.id));
|
|
1552
1911
|
const now = Date.now();
|
|
1553
1912
|
await this.db.runAsync(
|
|
1554
1913
|
`UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE entity_id = ? AND deleted_at IS NULL`,
|
|
@@ -1602,7 +1961,8 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1602
1961
|
let blobData = null;
|
|
1603
1962
|
if (rawBlob !== null && rawBlob.byteLength > 0 && rawBlob.byteLength % 4 === 0) {
|
|
1604
1963
|
const copy = new ArrayBuffer(rawBlob.byteLength);
|
|
1605
|
-
new Uint8Array(copy)
|
|
1964
|
+
const alignedBlob = new Uint8Array(copy);
|
|
1965
|
+
alignedBlob.set(rawBlob);
|
|
1606
1966
|
const floats = new Float32Array(copy, 0, rawBlob.byteLength / 4);
|
|
1607
1967
|
let allFinite = true;
|
|
1608
1968
|
for (let i = 0; i < floats.length; i++) {
|
|
@@ -1612,7 +1972,7 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1612
1972
|
}
|
|
1613
1973
|
}
|
|
1614
1974
|
if (allFinite) {
|
|
1615
|
-
blobData =
|
|
1975
|
+
blobData = alignedBlob;
|
|
1616
1976
|
}
|
|
1617
1977
|
}
|
|
1618
1978
|
if (existing) {
|
|
@@ -1628,7 +1988,7 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1628
1988
|
`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 = ?`,
|
|
1629
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]
|
|
1630
1990
|
);
|
|
1631
|
-
factsWithPreservedBlob.
|
|
1991
|
+
factsWithPreservedBlob.set(fact.id, blobData);
|
|
1632
1992
|
if (!fact.deleted_at) preservedBlobDims.add(blobData.byteLength / 4);
|
|
1633
1993
|
} else {
|
|
1634
1994
|
await this.db.runAsync(
|
|
@@ -1638,13 +1998,14 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1638
1998
|
}
|
|
1639
1999
|
existingFactsById.set(fact.id, { id: fact.id, entity_id: entityId, updated_at: safeUpdatedAt });
|
|
1640
2000
|
upsertedFactIds.add(fact.id);
|
|
2001
|
+
if (fact.deleted_at) upsertedDeletedFactIds.add(fact.id);
|
|
1641
2002
|
} else {
|
|
1642
2003
|
if (blobData != null) {
|
|
1643
2004
|
await this.db.runAsync(
|
|
1644
2005
|
`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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1645
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]
|
|
1646
2007
|
);
|
|
1647
|
-
factsWithPreservedBlob.
|
|
2008
|
+
factsWithPreservedBlob.set(fact.id, blobData);
|
|
1648
2009
|
if (!fact.deleted_at) preservedBlobDims.add(blobData.byteLength / 4);
|
|
1649
2010
|
} else {
|
|
1650
2011
|
await this.db.runAsync(
|
|
@@ -1654,6 +2015,7 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1654
2015
|
}
|
|
1655
2016
|
existingFactsById.set(fact.id, { id: fact.id, entity_id: entityId, updated_at: safeUpdatedAt });
|
|
1656
2017
|
upsertedFactIds.add(fact.id);
|
|
2018
|
+
if (fact.deleted_at) upsertedDeletedFactIds.add(fact.id);
|
|
1657
2019
|
}
|
|
1658
2020
|
}
|
|
1659
2021
|
const taskIds = bundle.tasks.map((task) => task.id);
|
|
@@ -1709,12 +2071,34 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1709
2071
|
if (!fact.deleted_at && upsertedFactIds.has(fact.id) && !factsWithPreservedBlob.has(fact.id)) {
|
|
1710
2072
|
await this.embedFact({
|
|
1711
2073
|
id: fact.id,
|
|
2074
|
+
entity_id: entityId,
|
|
2075
|
+
// Use authoritative entityId from dump key, not fact.entity_id
|
|
1712
2076
|
title: fact.title,
|
|
1713
2077
|
body: fact.body,
|
|
1714
2078
|
tags: Array.isArray(fact.tags) || typeof fact.tags === "string" ? fact.tags : []
|
|
1715
2079
|
});
|
|
1716
2080
|
}
|
|
1717
2081
|
}
|
|
2082
|
+
for (const fact of bundle.facts) {
|
|
2083
|
+
const blobData = factsWithPreservedBlob.get(fact.id);
|
|
2084
|
+
if (blobData && !fact.deleted_at && upsertedFactIds.has(fact.id)) {
|
|
2085
|
+
try {
|
|
2086
|
+
const float32Vector = new Float32Array(blobData.buffer, blobData.byteOffset, blobData.byteLength / 4);
|
|
2087
|
+
await this._notifyEmbeddingPersisted(entityId, fact.id, float32Vector);
|
|
2088
|
+
} catch (hookErr) {
|
|
2089
|
+
console.warn(`[WikiMemory] onEmbeddingPersisted hook failed for preserved-blob fact ${fact.id}:`, hookErr);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
for (const factId of softDeletedFactIds) {
|
|
2094
|
+
if (!upsertedFactIds.has(factId) || upsertedDeletedFactIds.has(factId)) {
|
|
2095
|
+
try {
|
|
2096
|
+
await this._notifyEmbeddingPersisted(entityId, factId, null);
|
|
2097
|
+
} catch (hookErr) {
|
|
2098
|
+
console.warn(`[WikiMemory] onEmbeddingPersisted(vector=null) hook failed for soft-deleted fact ${factId}:`, hookErr);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
1718
2102
|
try {
|
|
1719
2103
|
const canonicalRow = await this.db.getFirstAsync(
|
|
1720
2104
|
`SELECT value FROM ${this.prefix}meta WHERE key = 'embedding_dimension'`
|
|
@@ -1785,7 +2169,13 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1785
2169
|
const now = Date.now();
|
|
1786
2170
|
let deletedEntries = 0;
|
|
1787
2171
|
let deletedTasks = 0;
|
|
2172
|
+
const deletedEntryIds = [];
|
|
1788
2173
|
if (params.clearAll) {
|
|
2174
|
+
const entriesToDelete = await this.db.getAllAsync(
|
|
2175
|
+
`SELECT id FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`,
|
|
2176
|
+
[entityId]
|
|
2177
|
+
);
|
|
2178
|
+
deletedEntryIds.push(...entriesToDelete.map((e) => e.id));
|
|
1789
2179
|
const [entriesRes, tasksRes] = await Promise.all([
|
|
1790
2180
|
this.db.runAsync(`UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE entity_id = ? AND deleted_at IS NULL`, [now, now, entityId]),
|
|
1791
2181
|
this.db.runAsync(`UPDATE ${this.prefix}tasks SET deleted_at = ?, updated_at = ? WHERE entity_id = ? AND deleted_at IS NULL`, [now, now, entityId])
|
|
@@ -1803,6 +2193,27 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1803
2193
|
if (params.sourceRef !== void 0 && !sourceRef) throw new Error("Invalid sourceRef");
|
|
1804
2194
|
const sourceHash = params.sourceHash !== void 0 ? normalizeSourceHash(params.sourceHash) : null;
|
|
1805
2195
|
if (params.sourceHash !== void 0 && !sourceHash) throw new Error("Invalid sourceHash (must be 64-char hex string)");
|
|
2196
|
+
if (params.entryId) {
|
|
2197
|
+
const entry = await this.db.getFirstAsync(
|
|
2198
|
+
`SELECT id FROM ${this.prefix}entries WHERE id = ? AND entity_id = ? AND deleted_at IS NULL`,
|
|
2199
|
+
[params.entryId, entityId]
|
|
2200
|
+
);
|
|
2201
|
+
if (entry) deletedEntryIds.push(entry.id);
|
|
2202
|
+
}
|
|
2203
|
+
if (sourceRef || sourceHash) {
|
|
2204
|
+
let q = `SELECT id FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`;
|
|
2205
|
+
const args = [entityId];
|
|
2206
|
+
if (sourceRef) {
|
|
2207
|
+
q += ` AND source_ref = ?`;
|
|
2208
|
+
args.push(sourceRef);
|
|
2209
|
+
}
|
|
2210
|
+
if (sourceHash) {
|
|
2211
|
+
q += ` AND source_hash = ?`;
|
|
2212
|
+
args.push(sourceHash);
|
|
2213
|
+
}
|
|
2214
|
+
const entriesToDelete = await this.db.getAllAsync(q, args);
|
|
2215
|
+
deletedEntryIds.push(...entriesToDelete.map((e) => e.id));
|
|
2216
|
+
}
|
|
1806
2217
|
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;
|
|
1807
2218
|
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;
|
|
1808
2219
|
let refPromise = null;
|
|
@@ -1830,6 +2241,14 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1830
2241
|
}
|
|
1831
2242
|
await this.rebuildMiniSearchIndex(entityId);
|
|
1832
2243
|
this.vectorCache.delete(entityId);
|
|
2244
|
+
const uniqueDeletedIds = Array.from(new Set(deletedEntryIds));
|
|
2245
|
+
for (const factId of uniqueDeletedIds) {
|
|
2246
|
+
try {
|
|
2247
|
+
await this._notifyEmbeddingPersisted(entityId, factId, null);
|
|
2248
|
+
} catch (hookErr) {
|
|
2249
|
+
console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during forget for ${factId}:`, hookErr);
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
1833
2252
|
return { deleted: { entries: deletedEntries, tasks: deletedTasks } };
|
|
1834
2253
|
} finally {
|
|
1835
2254
|
this.activeMaintenanceJobs.delete(forgetKey);
|
|
@@ -1899,7 +2318,15 @@ ${chunk}`;
|
|
|
1899
2318
|
}
|
|
1900
2319
|
const now = Date.now();
|
|
1901
2320
|
const insertedFacts = [];
|
|
2321
|
+
const deletedSourceFactIds = [];
|
|
1902
2322
|
await this.db.withTransactionAsync(async () => {
|
|
2323
|
+
const existingSourceFacts = await this.db.getAllAsync(
|
|
2324
|
+
`SELECT id FROM ${this.prefix}entries WHERE source_ref = ? AND entity_id = ? AND deleted_at IS NULL`,
|
|
2325
|
+
[sourceRef, entityId]
|
|
2326
|
+
);
|
|
2327
|
+
for (const row of existingSourceFacts) {
|
|
2328
|
+
deletedSourceFactIds.push(row.id);
|
|
2329
|
+
}
|
|
1903
2330
|
await this.db.runAsync(
|
|
1904
2331
|
`UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE source_ref = ? AND entity_id = ? AND deleted_at IS NULL`,
|
|
1905
2332
|
[now, now, sourceRef, entityId]
|
|
@@ -1911,11 +2338,19 @@ ${chunk}`;
|
|
|
1911
2338
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1912
2339
|
[id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "user_document", sourceHash, sourceRef, now, now]
|
|
1913
2340
|
);
|
|
1914
|
-
insertedFacts.push({ id, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
2341
|
+
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
1915
2342
|
}
|
|
1916
2343
|
});
|
|
1917
2344
|
await this.rebuildMiniSearchIndex(entityId);
|
|
1918
2345
|
this.vectorCache.delete(entityId);
|
|
2346
|
+
const uniqueDeletedSourceFactIds = Array.from(new Set(deletedSourceFactIds));
|
|
2347
|
+
for (const factId of uniqueDeletedSourceFactIds) {
|
|
2348
|
+
try {
|
|
2349
|
+
await this._notifyEmbeddingPersisted(entityId, factId, null);
|
|
2350
|
+
} catch (hookErr) {
|
|
2351
|
+
console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during ingest for ${factId}:`, hookErr);
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
1919
2354
|
for (const fact of insertedFacts) {
|
|
1920
2355
|
await this.embedFact(fact);
|
|
1921
2356
|
}
|