@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.mjs
CHANGED
|
@@ -554,6 +554,11 @@ var _WikiMemory = class _WikiMemory {
|
|
|
554
554
|
`UPDATE ${this.prefix}entries SET embedding_blob = ?, embedding = NULL WHERE id = ?`,
|
|
555
555
|
[blob, fact.id]
|
|
556
556
|
);
|
|
557
|
+
try {
|
|
558
|
+
await this._notifyEmbeddingPersisted(fact.entity_id, fact.id, float32Vector);
|
|
559
|
+
} catch (hookErr) {
|
|
560
|
+
console.warn(`[WikiMemory] onEmbeddingPersisted hook failed for ${fact.id}:`, hookErr);
|
|
561
|
+
}
|
|
557
562
|
return true;
|
|
558
563
|
} catch (err) {
|
|
559
564
|
console.warn(`[WikiMemory] embedFact failed for ${fact.id}:`, err);
|
|
@@ -569,6 +574,9 @@ var _WikiMemory = class _WikiMemory {
|
|
|
569
574
|
_warnCrossEntityCollision(type, id, existingEntityId, targetEntityId) {
|
|
570
575
|
console.warn(`[WikiMemory] importDump: ${type} id "${id}" already belongs to entity "${existingEntityId}"; skipping for entity "${targetEntityId}"`);
|
|
571
576
|
}
|
|
577
|
+
async _notifyEmbeddingPersisted(entityId, factId, vector) {
|
|
578
|
+
await this.options.vectorRanker?.onEmbeddingPersisted?.({ entityId, factId, vector });
|
|
579
|
+
}
|
|
572
580
|
async setup() {
|
|
573
581
|
const entriesExistedBeforeSetup = await this.db.getFirstAsync(
|
|
574
582
|
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
|
|
@@ -751,8 +759,15 @@ var _WikiMemory = class _WikiMemory {
|
|
|
751
759
|
let deletedEntries = 0;
|
|
752
760
|
let deletedTasks = 0;
|
|
753
761
|
let deletedEvents = 0;
|
|
762
|
+
const deletedEntryIds = [];
|
|
754
763
|
if (retainSoftDeletedFor !== null) {
|
|
755
764
|
const cutoff = now - retainSoftDeletedFor * 864e5;
|
|
765
|
+
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 < ?`,
|
|
768
|
+
[entityId, cutoff]
|
|
769
|
+
);
|
|
770
|
+
deletedEntryIds.push(...entriesToDelete.map((e) => e.id));
|
|
756
771
|
const entryResult = await this.db.runAsync(
|
|
757
772
|
`DELETE FROM ${this.prefix}entries
|
|
758
773
|
WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at < ?`,
|
|
@@ -781,6 +796,14 @@ var _WikiMemory = class _WikiMemory {
|
|
|
781
796
|
}
|
|
782
797
|
await this.rebuildMiniSearchIndex(entityId);
|
|
783
798
|
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
|
+
}
|
|
784
807
|
return { entries: deletedEntries, tasks: deletedTasks, events: deletedEvents };
|
|
785
808
|
} finally {
|
|
786
809
|
this.activeMaintenanceJobs.delete(pruneKey);
|
|
@@ -801,6 +824,10 @@ var _WikiMemory = class _WikiMemory {
|
|
|
801
824
|
if (maxResults === 0) ; else if (trimmedQuery) {
|
|
802
825
|
let usedEmbed = false;
|
|
803
826
|
if (!skipEmbed && embedFn) {
|
|
827
|
+
let rankerShouldRethrow = false;
|
|
828
|
+
let pendingRankerFallbackError;
|
|
829
|
+
let usedKeywordFallback = false;
|
|
830
|
+
let scoredAlreadySortedAndLimited = false;
|
|
804
831
|
try {
|
|
805
832
|
const queryVec = await embedFn(trimmedQuery);
|
|
806
833
|
if (queryVec.length === 0 || !queryVec.every((v) => typeof v === "number" && isFinite(v))) {
|
|
@@ -832,6 +859,7 @@ var _WikiMemory = class _WikiMemory {
|
|
|
832
859
|
`Some facts have embeddings that do not match the current model dimension. Call runReembed() to rebuild all embeddings consistently.`
|
|
833
860
|
);
|
|
834
861
|
}
|
|
862
|
+
const useRanker = Boolean(this.options.vectorRanker);
|
|
835
863
|
let candidateRows;
|
|
836
864
|
let populateCache = true;
|
|
837
865
|
let miniSearchScores;
|
|
@@ -850,15 +878,30 @@ var _WikiMemory = class _WikiMemory {
|
|
|
850
878
|
} else {
|
|
851
879
|
const topKIds = topKResults.map((r) => r.id);
|
|
852
880
|
const inClauseChunkSize = 500;
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
881
|
+
if (useRanker) {
|
|
882
|
+
const rows = [];
|
|
883
|
+
for (let i = 0; i < topKIds.length; i += inClauseChunkSize) {
|
|
884
|
+
const idChunk = topKIds.slice(i, i + inClauseChunkSize);
|
|
885
|
+
const placeholders = idChunk.map(() => "?").join(",");
|
|
886
|
+
const chunkRows = await this.db.getAllAsync(
|
|
887
|
+
`SELECT id, updated_at, access_count FROM ${this.prefix}entries WHERE id IN (${placeholders}) AND deleted_at IS NULL`,
|
|
888
|
+
idChunk
|
|
889
|
+
);
|
|
890
|
+
rows.push(...chunkRows);
|
|
891
|
+
}
|
|
892
|
+
candidateRows = rows;
|
|
893
|
+
} else {
|
|
894
|
+
const rows = [];
|
|
895
|
+
for (let i = 0; i < topKIds.length; i += inClauseChunkSize) {
|
|
896
|
+
const idChunk = topKIds.slice(i, i + inClauseChunkSize);
|
|
897
|
+
const placeholders = idChunk.map(() => "?").join(",");
|
|
898
|
+
const chunkRows = await this.db.getAllAsync(
|
|
899
|
+
`SELECT id, embedding_blob, embedding, updated_at, access_count FROM ${this.prefix}entries WHERE id IN (${placeholders}) AND deleted_at IS NULL`,
|
|
900
|
+
idChunk
|
|
901
|
+
);
|
|
902
|
+
rows.push(...chunkRows);
|
|
903
|
+
}
|
|
904
|
+
candidateRows = rows;
|
|
862
905
|
}
|
|
863
906
|
if (weight !== void 0 && weight < 1) {
|
|
864
907
|
const maxMsScore = Math.max(1, topKResults[0]?.score ?? 1);
|
|
@@ -867,10 +910,17 @@ var _WikiMemory = class _WikiMemory {
|
|
|
867
910
|
}
|
|
868
911
|
}
|
|
869
912
|
} else {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
913
|
+
if (useRanker) {
|
|
914
|
+
candidateRows = await this.db.getAllAsync(
|
|
915
|
+
`SELECT id, updated_at, access_count FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`,
|
|
916
|
+
[entityId]
|
|
917
|
+
);
|
|
918
|
+
} else {
|
|
919
|
+
candidateRows = await this.db.getAllAsync(
|
|
920
|
+
`SELECT id, embedding_blob, embedding, updated_at, access_count FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`,
|
|
921
|
+
[entityId]
|
|
922
|
+
);
|
|
923
|
+
}
|
|
874
924
|
if (weight !== void 0 && weight < 1) {
|
|
875
925
|
const msResults = this.miniSearch.search(trimmedQuery, {
|
|
876
926
|
filter: (r) => r.entity_id === entityId,
|
|
@@ -883,76 +933,263 @@ var _WikiMemory = class _WikiMemory {
|
|
|
883
933
|
if (candidateRows === null) {
|
|
884
934
|
usedEmbed = true;
|
|
885
935
|
} else {
|
|
886
|
-
let
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
936
|
+
let scored;
|
|
937
|
+
if (useRanker) {
|
|
938
|
+
const candidateIds = effectivePreFilterLimit !== void 0 ? candidateRows.map((r) => r.id) : void 0;
|
|
939
|
+
try {
|
|
940
|
+
const oversampledLimit = Math.max(maxResults * 2, maxResults + 50);
|
|
941
|
+
scored = await this._rankWithVectorRanker({
|
|
942
|
+
entityId,
|
|
943
|
+
queryVec,
|
|
944
|
+
candidateIds,
|
|
945
|
+
weight,
|
|
946
|
+
miniSearchScores,
|
|
947
|
+
limit: oversampledLimit
|
|
948
|
+
});
|
|
949
|
+
if (scored.length > 0) {
|
|
950
|
+
const scoredIds2 = new Set(scored.map((s) => s.id));
|
|
951
|
+
const metaMap = /* @__PURE__ */ new Map();
|
|
952
|
+
for (const r of candidateRows) {
|
|
953
|
+
if (scoredIds2.has(r.id)) {
|
|
954
|
+
metaMap.set(r.id, { updated_at: r.updated_at, access_count: r.access_count });
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
scored = scored.map((s) => {
|
|
958
|
+
const meta = metaMap.get(s.id);
|
|
959
|
+
return { ...s, updated_at: meta?.updated_at ?? null, access_count: meta?.access_count ?? null };
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
const scoredIds = new Set(scored.map((s) => s.id));
|
|
963
|
+
const isHybrid = weight !== void 0 && weight < 1;
|
|
964
|
+
const maxBackfill = isHybrid ? maxResults : Math.max(0, maxResults - scored.length);
|
|
965
|
+
if (maxBackfill > 0) {
|
|
966
|
+
if (isHybrid) {
|
|
967
|
+
const topK = [];
|
|
968
|
+
for (const row of candidateRows) {
|
|
969
|
+
if (scoredIds.has(row.id)) continue;
|
|
970
|
+
const kwScore = miniSearchScores?.get(row.id) ?? 0;
|
|
971
|
+
const candidate = { row, kwScore };
|
|
972
|
+
if (topK.length < maxBackfill) {
|
|
973
|
+
let insertIdx = topK.length;
|
|
974
|
+
for (let i = 0; i < topK.length; i++) {
|
|
975
|
+
const cmp = this._compareScoredRows(
|
|
976
|
+
{
|
|
977
|
+
id: candidate.row.id,
|
|
978
|
+
score: candidate.kwScore,
|
|
979
|
+
updated_at: candidate.row.updated_at,
|
|
980
|
+
access_count: candidate.row.access_count
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
id: topK[i].row.id,
|
|
984
|
+
score: topK[i].kwScore,
|
|
985
|
+
updated_at: topK[i].row.updated_at,
|
|
986
|
+
access_count: topK[i].row.access_count
|
|
987
|
+
}
|
|
988
|
+
);
|
|
989
|
+
if (cmp < 0) {
|
|
990
|
+
insertIdx = i;
|
|
991
|
+
break;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
topK.splice(insertIdx, 0, candidate);
|
|
995
|
+
} else {
|
|
996
|
+
const cmpWorst = this._compareScoredRows(
|
|
997
|
+
{
|
|
998
|
+
id: candidate.row.id,
|
|
999
|
+
score: candidate.kwScore,
|
|
1000
|
+
updated_at: candidate.row.updated_at,
|
|
1001
|
+
access_count: candidate.row.access_count
|
|
1002
|
+
},
|
|
1003
|
+
{
|
|
1004
|
+
id: topK[maxBackfill - 1].row.id,
|
|
1005
|
+
score: topK[maxBackfill - 1].kwScore,
|
|
1006
|
+
updated_at: topK[maxBackfill - 1].row.updated_at,
|
|
1007
|
+
access_count: topK[maxBackfill - 1].row.access_count
|
|
1008
|
+
}
|
|
1009
|
+
);
|
|
1010
|
+
if (cmpWorst < 0) {
|
|
1011
|
+
let insertIdx = maxBackfill - 1;
|
|
1012
|
+
for (let i = 0; i < topK.length; i++) {
|
|
1013
|
+
const cmp = this._compareScoredRows(
|
|
1014
|
+
{
|
|
1015
|
+
id: candidate.row.id,
|
|
1016
|
+
score: candidate.kwScore,
|
|
1017
|
+
updated_at: candidate.row.updated_at,
|
|
1018
|
+
access_count: candidate.row.access_count
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
id: topK[i].row.id,
|
|
1022
|
+
score: topK[i].kwScore,
|
|
1023
|
+
updated_at: topK[i].row.updated_at,
|
|
1024
|
+
access_count: topK[i].row.access_count
|
|
1025
|
+
}
|
|
1026
|
+
);
|
|
1027
|
+
if (cmp < 0) {
|
|
1028
|
+
insertIdx = i;
|
|
1029
|
+
break;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
topK.splice(insertIdx, 0, candidate);
|
|
1033
|
+
topK.pop();
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
for (const { row, kwScore } of topK) {
|
|
1038
|
+
scored.push({
|
|
1039
|
+
id: row.id,
|
|
1040
|
+
score: (1 - weight) * kwScore,
|
|
1041
|
+
updated_at: row.updated_at,
|
|
1042
|
+
access_count: row.access_count
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
} else {
|
|
1046
|
+
const omitted = [];
|
|
1047
|
+
for (const row of candidateRows) {
|
|
1048
|
+
if (scoredIds.has(row.id)) continue;
|
|
1049
|
+
omitted.push({ id: row.id, score: -2, updated_at: row.updated_at, access_count: row.access_count });
|
|
1050
|
+
}
|
|
1051
|
+
if (omitted.length > 0) {
|
|
1052
|
+
this._tieBreakSort(omitted);
|
|
1053
|
+
scored.push(...omitted.slice(0, maxBackfill));
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
} catch (rankerErr) {
|
|
1058
|
+
const rankerError = rankerErr instanceof Error ? rankerErr : new Error(String(rankerErr));
|
|
1059
|
+
const policy = this.options.vectorRankerFallback ?? "js-cosine";
|
|
1060
|
+
this.options.onVectorRankerFallback?.({ error: rankerError, policy });
|
|
1061
|
+
if (policy === "throw") {
|
|
1062
|
+
rankerShouldRethrow = true;
|
|
1063
|
+
throw rankerError;
|
|
1064
|
+
} else if (policy === "js-cosine") {
|
|
1065
|
+
let fallbackRows = candidateRows;
|
|
1066
|
+
if (fallbackRows && fallbackRows.length > 0 && !("embedding_blob" in fallbackRows[0])) {
|
|
1067
|
+
const rowIds = fallbackRows.map((r) => r.id);
|
|
1068
|
+
const embeddingsMap = /* @__PURE__ */ new Map();
|
|
1069
|
+
const chunkSize = 500;
|
|
1070
|
+
for (let i = 0; i < rowIds.length; i += chunkSize) {
|
|
1071
|
+
const idChunk = rowIds.slice(i, i + chunkSize);
|
|
1072
|
+
const placeholders = idChunk.map(() => "?").join(",");
|
|
1073
|
+
const embeddingRows = await this.db.getAllAsync(
|
|
1074
|
+
`SELECT id, embedding_blob, embedding FROM ${this.prefix}entries WHERE id IN (${placeholders}) AND entity_id = ? AND deleted_at IS NULL`,
|
|
1075
|
+
[...idChunk, entityId]
|
|
1076
|
+
);
|
|
1077
|
+
for (const row of embeddingRows) {
|
|
1078
|
+
embeddingsMap.set(row.id, { embedding_blob: row.embedding_blob, embedding: row.embedding });
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
fallbackRows = fallbackRows.map((r) => ({
|
|
1082
|
+
...r,
|
|
1083
|
+
embedding_blob: embeddingsMap.get(r.id)?.embedding_blob ?? null,
|
|
1084
|
+
embedding: embeddingsMap.get(r.id)?.embedding ?? null
|
|
1085
|
+
}));
|
|
1086
|
+
}
|
|
1087
|
+
scored = await this._rankWithJsCosine({
|
|
1088
|
+
entityId,
|
|
1089
|
+
queryVec,
|
|
1090
|
+
candidateRows: fallbackRows,
|
|
1091
|
+
weight,
|
|
1092
|
+
miniSearchScores,
|
|
1093
|
+
populateCache,
|
|
1094
|
+
limit: maxResults
|
|
1095
|
+
});
|
|
1096
|
+
scoredAlreadySortedAndLimited = true;
|
|
1097
|
+
} else if (policy === "keyword") {
|
|
1098
|
+
const msResults = this.miniSearch.search(trimmedQuery, {
|
|
1099
|
+
filter: (r) => r.entity_id === entityId,
|
|
1100
|
+
combineWith: "OR"
|
|
1101
|
+
});
|
|
1102
|
+
const topResults = msResults.slice(0, maxResults);
|
|
1103
|
+
const resultIds = new Set(topResults.map((r) => r.id));
|
|
1104
|
+
const candidateMap = /* @__PURE__ */ new Map();
|
|
1105
|
+
for (const r of candidateRows) {
|
|
1106
|
+
if (resultIds.has(r.id)) {
|
|
1107
|
+
candidateMap.set(r.id, { updated_at: r.updated_at, access_count: r.access_count });
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
scored = topResults.map((r) => {
|
|
1111
|
+
const meta = candidateMap.get(r.id);
|
|
1112
|
+
return {
|
|
1113
|
+
id: r.id,
|
|
1114
|
+
score: r.score ?? 0,
|
|
1115
|
+
access_count: meta?.access_count ?? null,
|
|
1116
|
+
updated_at: meta?.updated_at ?? null
|
|
1117
|
+
};
|
|
1118
|
+
});
|
|
1119
|
+
usedKeywordFallback = true;
|
|
907
1120
|
} else {
|
|
908
|
-
|
|
1121
|
+
scored = [];
|
|
909
1122
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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);
|
|
1123
|
+
if (this.options.propagateRankerFailureToRetrievalFallback) {
|
|
1124
|
+
const mirrored = new Error("Vector ranker failed, falling back");
|
|
1125
|
+
mirrored.cause = rankerError;
|
|
1126
|
+
pendingRankerFallbackError = mirrored;
|
|
923
1127
|
}
|
|
924
|
-
this.vectorCache.set(entityId, entityCache);
|
|
925
1128
|
}
|
|
1129
|
+
} else {
|
|
1130
|
+
scored = await this._rankWithJsCosine({
|
|
1131
|
+
entityId,
|
|
1132
|
+
queryVec,
|
|
1133
|
+
candidateRows,
|
|
1134
|
+
weight,
|
|
1135
|
+
miniSearchScores,
|
|
1136
|
+
populateCache,
|
|
1137
|
+
limit: maxResults
|
|
1138
|
+
});
|
|
1139
|
+
scoredAlreadySortedAndLimited = true;
|
|
926
1140
|
}
|
|
927
|
-
scored.
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
)
|
|
947
|
-
|
|
1141
|
+
if (scored.length > 0) {
|
|
1142
|
+
if (!usedKeywordFallback && !scoredAlreadySortedAndLimited) {
|
|
1143
|
+
this._tieBreakSort(scored);
|
|
1144
|
+
}
|
|
1145
|
+
const topIds = (scoredAlreadySortedAndLimited ? scored : scored.slice(0, maxResults)).map((s) => s.id);
|
|
1146
|
+
if (topIds.length > 0) {
|
|
1147
|
+
const fullRows = [];
|
|
1148
|
+
const phase2ChunkSize = 500;
|
|
1149
|
+
for (let i = 0; i < topIds.length; i += phase2ChunkSize) {
|
|
1150
|
+
const idChunk = topIds.slice(i, i + phase2ChunkSize);
|
|
1151
|
+
const placeholders = idChunk.map(() => "?").join(",");
|
|
1152
|
+
const chunkRows = await this.db.getAllAsync(
|
|
1153
|
+
`SELECT * FROM ${this.prefix}entries WHERE id IN (${placeholders}) AND entity_id = ? AND deleted_at IS NULL`,
|
|
1154
|
+
[...idChunk, entityId]
|
|
1155
|
+
);
|
|
1156
|
+
fullRows.push(...chunkRows);
|
|
1157
|
+
}
|
|
1158
|
+
const byId = new Map(fullRows.map((r) => [r.id, r]));
|
|
1159
|
+
facts = topIds.map((id) => byId.get(id)).filter((f) => f !== void 0);
|
|
1160
|
+
if (facts.length < topIds.length) {
|
|
1161
|
+
const missingIds = topIds.filter((id) => !byId.has(id));
|
|
1162
|
+
const missingCount = missingIds.length;
|
|
1163
|
+
const sample = missingIds.slice(0, 5);
|
|
1164
|
+
const sampleSuffix = sample.length > 0 ? ` Missing ID sample: ${sample.join(", ")}${missingIds.length > sample.length ? ", ..." : ""}.` : "";
|
|
1165
|
+
const error = new Error(
|
|
1166
|
+
`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
|
|
1167
|
+
);
|
|
1168
|
+
this.options.onRetrievalFallback?.(error);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
if (pendingRankerFallbackError) {
|
|
1172
|
+
this.options.onRetrievalFallback?.(pendingRankerFallbackError);
|
|
1173
|
+
pendingRankerFallbackError = void 0;
|
|
1174
|
+
}
|
|
1175
|
+
usedEmbed = true;
|
|
1176
|
+
} else {
|
|
1177
|
+
if (pendingRankerFallbackError) {
|
|
1178
|
+
this.options.onRetrievalFallback?.(pendingRankerFallbackError);
|
|
1179
|
+
pendingRankerFallbackError = void 0;
|
|
948
1180
|
}
|
|
949
|
-
|
|
950
|
-
facts = topIds.map((id) => byId.get(id)).filter((f) => f !== void 0);
|
|
1181
|
+
usedEmbed = true;
|
|
951
1182
|
}
|
|
952
|
-
usedEmbed = true;
|
|
953
1183
|
}
|
|
954
1184
|
} catch (err) {
|
|
955
1185
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
1186
|
+
if (rankerShouldRethrow) {
|
|
1187
|
+
throw error;
|
|
1188
|
+
}
|
|
1189
|
+
if (pendingRankerFallbackError) {
|
|
1190
|
+
error.cause = pendingRankerFallbackError;
|
|
1191
|
+
pendingRankerFallbackError = void 0;
|
|
1192
|
+
}
|
|
956
1193
|
this.options.onRetrievalFallback?.(error);
|
|
957
1194
|
}
|
|
958
1195
|
}
|
|
@@ -969,8 +1206,8 @@ var _WikiMemory = class _WikiMemory {
|
|
|
969
1206
|
const idChunk = topIds.slice(i, i + kwChunkSize);
|
|
970
1207
|
const placeholders = idChunk.map(() => "?").join(",");
|
|
971
1208
|
const chunkRows = await this.db.getAllAsync(
|
|
972
|
-
`SELECT * FROM ${this.prefix}entries WHERE id IN (${placeholders}) AND deleted_at IS NULL`,
|
|
973
|
-
idChunk
|
|
1209
|
+
`SELECT * FROM ${this.prefix}entries WHERE id IN (${placeholders}) AND entity_id = ? AND deleted_at IS NULL`,
|
|
1210
|
+
[...idChunk, entityId]
|
|
974
1211
|
);
|
|
975
1212
|
kwRows.push(...chunkRows);
|
|
976
1213
|
}
|
|
@@ -1026,6 +1263,113 @@ var _WikiMemory = class _WikiMemory {
|
|
|
1026
1263
|
});
|
|
1027
1264
|
return { facts: parsedFacts, tasks, events: events.reverse() };
|
|
1028
1265
|
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Stable tie-break sort: score desc → access_count desc → updated_at desc → id asc.
|
|
1268
|
+
*/
|
|
1269
|
+
_tieBreakSort(items) {
|
|
1270
|
+
items.sort((a, b) => this._compareScoredRows(a, b));
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Comparator for score + deterministic tie-break fields.
|
|
1274
|
+
* Negative return means "a ranks ahead of b" for descending score order.
|
|
1275
|
+
*/
|
|
1276
|
+
_compareScoredRows(a, b) {
|
|
1277
|
+
const scoreDiff = b.score - a.score;
|
|
1278
|
+
if (scoreDiff !== 0) return scoreDiff;
|
|
1279
|
+
const accessCountDiff = (b.access_count ?? 0) - (a.access_count ?? 0);
|
|
1280
|
+
if (accessCountDiff !== 0) return accessCountDiff;
|
|
1281
|
+
const updatedAtDiff = (b.updated_at ?? 0) - (a.updated_at ?? 0);
|
|
1282
|
+
if (updatedAtDiff !== 0) return updatedAtDiff;
|
|
1283
|
+
return a.id.localeCompare(b.id);
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Score candidate rows using in-process JS cosine similarity.
|
|
1287
|
+
* Applies hybrid blending (if weight set) and tie-break sorting before returning.
|
|
1288
|
+
*/
|
|
1289
|
+
async _rankWithJsCosine(args) {
|
|
1290
|
+
const { entityId, queryVec, candidateRows, weight, miniSearchScores, populateCache, limit } = args;
|
|
1291
|
+
let entityCache = this.vectorCache.get(entityId);
|
|
1292
|
+
const tooLarge = populateCache && candidateRows.length > _WikiMemory.MAX_VECTOR_CACHE_FACTS_PER_ENTITY;
|
|
1293
|
+
if (tooLarge && entityCache) {
|
|
1294
|
+
this.vectorCache.delete(entityId);
|
|
1295
|
+
entityCache = void 0;
|
|
1296
|
+
}
|
|
1297
|
+
const canCache = populateCache && !tooLarge;
|
|
1298
|
+
if (canCache && !entityCache) {
|
|
1299
|
+
entityCache = /* @__PURE__ */ new Map();
|
|
1300
|
+
}
|
|
1301
|
+
const scored = candidateRows.map((row) => {
|
|
1302
|
+
let vector = entityCache?.get(row.id) ?? parseEmbedding(row.embedding_blob, row.embedding);
|
|
1303
|
+
if (vector && canCache && entityCache && !entityCache.has(row.id)) {
|
|
1304
|
+
entityCache.set(row.id, vector);
|
|
1305
|
+
}
|
|
1306
|
+
let score = 0;
|
|
1307
|
+
if (vector && vector.length === queryVec.length) {
|
|
1308
|
+
const cosSim = cosineSimilarity(queryVec, vector);
|
|
1309
|
+
if (weight !== void 0) {
|
|
1310
|
+
const kwScore = miniSearchScores?.get(row.id) ?? 0;
|
|
1311
|
+
score = weight * Math.max(0, cosSim) + (1 - weight) * kwScore;
|
|
1312
|
+
} else {
|
|
1313
|
+
score = cosSim;
|
|
1314
|
+
}
|
|
1315
|
+
} else if (weight !== void 0 && weight < 1) {
|
|
1316
|
+
const kwScore = miniSearchScores?.get(row.id) ?? 0;
|
|
1317
|
+
score = (1 - weight) * kwScore;
|
|
1318
|
+
} else {
|
|
1319
|
+
score = -2;
|
|
1320
|
+
}
|
|
1321
|
+
return { id: row.id, score, updated_at: row.updated_at, access_count: row.access_count };
|
|
1322
|
+
});
|
|
1323
|
+
if (canCache && entityCache && entityCache.size > 0) {
|
|
1324
|
+
if (!this.vectorCache.has(entityId)) {
|
|
1325
|
+
if (this.vectorCache.size >= _WikiMemory.MAX_VECTOR_CACHE_ENTITIES) {
|
|
1326
|
+
const oldestKey = this.vectorCache.keys().next().value;
|
|
1327
|
+
if (oldestKey !== void 0) this.vectorCache.delete(oldestKey);
|
|
1328
|
+
}
|
|
1329
|
+
this.vectorCache.set(entityId, entityCache);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
this._tieBreakSort(scored);
|
|
1333
|
+
return scored.slice(0, limit);
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Delegate semantic ranking to the injected VectorRanker.
|
|
1337
|
+
* Caller should pass an oversampledLimit to preserve recall after re-ranking.
|
|
1338
|
+
* Returns scored results ready for hybrid blending and tie-break sorting.
|
|
1339
|
+
*/
|
|
1340
|
+
async _rankWithVectorRanker(args) {
|
|
1341
|
+
const { entityId, queryVec, candidateIds, weight, miniSearchScores, limit } = args;
|
|
1342
|
+
const ranker = this.options.vectorRanker;
|
|
1343
|
+
if (!ranker) {
|
|
1344
|
+
throw new Error("vectorRanker not configured");
|
|
1345
|
+
}
|
|
1346
|
+
const rankerResults = await ranker.rankBySimilarity({
|
|
1347
|
+
entityId,
|
|
1348
|
+
queryVec,
|
|
1349
|
+
candidateIds,
|
|
1350
|
+
limit
|
|
1351
|
+
});
|
|
1352
|
+
const allowedIds = candidateIds ? new Set(candidateIds) : void 0;
|
|
1353
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1354
|
+
const normalized = [];
|
|
1355
|
+
for (const r of rankerResults) {
|
|
1356
|
+
if (normalized.length >= limit) break;
|
|
1357
|
+
if (seen.has(r.id)) continue;
|
|
1358
|
+
if (allowedIds && !allowedIds.has(r.id)) continue;
|
|
1359
|
+
if (!Number.isFinite(r.semanticScore)) continue;
|
|
1360
|
+
seen.add(r.id);
|
|
1361
|
+
normalized.push(r);
|
|
1362
|
+
}
|
|
1363
|
+
const scored = normalized.map((r) => {
|
|
1364
|
+
let score = r.semanticScore;
|
|
1365
|
+
if (weight !== void 0) {
|
|
1366
|
+
const kwScore = miniSearchScores?.get(r.id) ?? 0;
|
|
1367
|
+
score = weight * Math.max(0, r.semanticScore) + (1 - weight) * kwScore;
|
|
1368
|
+
}
|
|
1369
|
+
return { id: r.id, score };
|
|
1370
|
+
});
|
|
1371
|
+
return scored;
|
|
1372
|
+
}
|
|
1029
1373
|
async getMemoryBundle(entityId) {
|
|
1030
1374
|
return this._getFullBundle(entityId, { maxEvents: 10 });
|
|
1031
1375
|
}
|
|
@@ -1142,7 +1486,7 @@ ${JSON.stringify(currentFacts, null, 2)}`;
|
|
|
1142
1486
|
INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, created_at, updated_at)
|
|
1143
1487
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1144
1488
|
`, [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) });
|
|
1489
|
+
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
1146
1490
|
}
|
|
1147
1491
|
for (const task of validTasks) {
|
|
1148
1492
|
const id = generateId("task_");
|
|
@@ -1222,6 +1566,7 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1222
1566
|
const safeDeleted = deleted.filter((id) => mutableIds.has(id));
|
|
1223
1567
|
const validNewFacts = newFacts.map(validateFact).filter((f) => f !== null);
|
|
1224
1568
|
const insertedFacts = [];
|
|
1569
|
+
const uniqueDeletedFactIds = Array.from(new Set(safeDeleted));
|
|
1225
1570
|
await this.db.withTransactionAsync(async () => {
|
|
1226
1571
|
for (const id of safeDowngraded) {
|
|
1227
1572
|
await this.db.runAsync(`UPDATE ${this.prefix}entries SET confidence = 'tentative', updated_at = ? WHERE id = ? AND entity_id = ?`, [now, id, entityId]);
|
|
@@ -1235,11 +1580,18 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1235
1580
|
INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, created_at, updated_at)
|
|
1236
1581
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1237
1582
|
`, [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) });
|
|
1583
|
+
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
1239
1584
|
}
|
|
1240
1585
|
});
|
|
1241
1586
|
this.vectorCache.delete(entityId);
|
|
1242
1587
|
await this.rebuildMiniSearchIndex(entityId);
|
|
1588
|
+
for (const factId of uniqueDeletedFactIds) {
|
|
1589
|
+
try {
|
|
1590
|
+
await this._notifyEmbeddingPersisted(entityId, factId, null);
|
|
1591
|
+
} catch (hookErr) {
|
|
1592
|
+
console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during heal for ${factId}:`, hookErr);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1243
1595
|
for (const fact of insertedFacts) {
|
|
1244
1596
|
await this.embedFact(fact);
|
|
1245
1597
|
}
|
|
@@ -1539,10 +1891,17 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1539
1891
|
}
|
|
1540
1892
|
async _doImportEntity(entityId, bundle, merge) {
|
|
1541
1893
|
const upsertedFactIds = /* @__PURE__ */ new Set();
|
|
1542
|
-
const
|
|
1894
|
+
const upsertedDeletedFactIds = /* @__PURE__ */ new Set();
|
|
1895
|
+
const factsWithPreservedBlob = /* @__PURE__ */ new Map();
|
|
1543
1896
|
const preservedBlobDims = /* @__PURE__ */ new Set();
|
|
1897
|
+
const softDeletedFactIds = [];
|
|
1544
1898
|
await this.db.withTransactionAsync(async () => {
|
|
1545
1899
|
if (!merge) {
|
|
1900
|
+
const toDelete = await this.db.getAllAsync(
|
|
1901
|
+
`SELECT id FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`,
|
|
1902
|
+
[entityId]
|
|
1903
|
+
);
|
|
1904
|
+
softDeletedFactIds.push(...toDelete.map((r) => r.id));
|
|
1546
1905
|
const now = Date.now();
|
|
1547
1906
|
await this.db.runAsync(
|
|
1548
1907
|
`UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE entity_id = ? AND deleted_at IS NULL`,
|
|
@@ -1596,7 +1955,8 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1596
1955
|
let blobData = null;
|
|
1597
1956
|
if (rawBlob !== null && rawBlob.byteLength > 0 && rawBlob.byteLength % 4 === 0) {
|
|
1598
1957
|
const copy = new ArrayBuffer(rawBlob.byteLength);
|
|
1599
|
-
new Uint8Array(copy)
|
|
1958
|
+
const alignedBlob = new Uint8Array(copy);
|
|
1959
|
+
alignedBlob.set(rawBlob);
|
|
1600
1960
|
const floats = new Float32Array(copy, 0, rawBlob.byteLength / 4);
|
|
1601
1961
|
let allFinite = true;
|
|
1602
1962
|
for (let i = 0; i < floats.length; i++) {
|
|
@@ -1606,7 +1966,7 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1606
1966
|
}
|
|
1607
1967
|
}
|
|
1608
1968
|
if (allFinite) {
|
|
1609
|
-
blobData =
|
|
1969
|
+
blobData = alignedBlob;
|
|
1610
1970
|
}
|
|
1611
1971
|
}
|
|
1612
1972
|
if (existing) {
|
|
@@ -1622,7 +1982,7 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1622
1982
|
`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
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]
|
|
1624
1984
|
);
|
|
1625
|
-
factsWithPreservedBlob.
|
|
1985
|
+
factsWithPreservedBlob.set(fact.id, blobData);
|
|
1626
1986
|
if (!fact.deleted_at) preservedBlobDims.add(blobData.byteLength / 4);
|
|
1627
1987
|
} else {
|
|
1628
1988
|
await this.db.runAsync(
|
|
@@ -1632,13 +1992,14 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1632
1992
|
}
|
|
1633
1993
|
existingFactsById.set(fact.id, { id: fact.id, entity_id: entityId, updated_at: safeUpdatedAt });
|
|
1634
1994
|
upsertedFactIds.add(fact.id);
|
|
1995
|
+
if (fact.deleted_at) upsertedDeletedFactIds.add(fact.id);
|
|
1635
1996
|
} else {
|
|
1636
1997
|
if (blobData != null) {
|
|
1637
1998
|
await this.db.runAsync(
|
|
1638
1999
|
`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
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]
|
|
1640
2001
|
);
|
|
1641
|
-
factsWithPreservedBlob.
|
|
2002
|
+
factsWithPreservedBlob.set(fact.id, blobData);
|
|
1642
2003
|
if (!fact.deleted_at) preservedBlobDims.add(blobData.byteLength / 4);
|
|
1643
2004
|
} else {
|
|
1644
2005
|
await this.db.runAsync(
|
|
@@ -1648,6 +2009,7 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1648
2009
|
}
|
|
1649
2010
|
existingFactsById.set(fact.id, { id: fact.id, entity_id: entityId, updated_at: safeUpdatedAt });
|
|
1650
2011
|
upsertedFactIds.add(fact.id);
|
|
2012
|
+
if (fact.deleted_at) upsertedDeletedFactIds.add(fact.id);
|
|
1651
2013
|
}
|
|
1652
2014
|
}
|
|
1653
2015
|
const taskIds = bundle.tasks.map((task) => task.id);
|
|
@@ -1703,12 +2065,34 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1703
2065
|
if (!fact.deleted_at && upsertedFactIds.has(fact.id) && !factsWithPreservedBlob.has(fact.id)) {
|
|
1704
2066
|
await this.embedFact({
|
|
1705
2067
|
id: fact.id,
|
|
2068
|
+
entity_id: entityId,
|
|
2069
|
+
// Use authoritative entityId from dump key, not fact.entity_id
|
|
1706
2070
|
title: fact.title,
|
|
1707
2071
|
body: fact.body,
|
|
1708
2072
|
tags: Array.isArray(fact.tags) || typeof fact.tags === "string" ? fact.tags : []
|
|
1709
2073
|
});
|
|
1710
2074
|
}
|
|
1711
2075
|
}
|
|
2076
|
+
for (const fact of bundle.facts) {
|
|
2077
|
+
const blobData = factsWithPreservedBlob.get(fact.id);
|
|
2078
|
+
if (blobData && !fact.deleted_at && upsertedFactIds.has(fact.id)) {
|
|
2079
|
+
try {
|
|
2080
|
+
const float32Vector = new Float32Array(blobData.buffer, blobData.byteOffset, blobData.byteLength / 4);
|
|
2081
|
+
await this._notifyEmbeddingPersisted(entityId, fact.id, float32Vector);
|
|
2082
|
+
} catch (hookErr) {
|
|
2083
|
+
console.warn(`[WikiMemory] onEmbeddingPersisted hook failed for preserved-blob fact ${fact.id}:`, hookErr);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
for (const factId of softDeletedFactIds) {
|
|
2088
|
+
if (!upsertedFactIds.has(factId) || upsertedDeletedFactIds.has(factId)) {
|
|
2089
|
+
try {
|
|
2090
|
+
await this._notifyEmbeddingPersisted(entityId, factId, null);
|
|
2091
|
+
} catch (hookErr) {
|
|
2092
|
+
console.warn(`[WikiMemory] onEmbeddingPersisted(vector=null) hook failed for soft-deleted fact ${factId}:`, hookErr);
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
1712
2096
|
try {
|
|
1713
2097
|
const canonicalRow = await this.db.getFirstAsync(
|
|
1714
2098
|
`SELECT value FROM ${this.prefix}meta WHERE key = 'embedding_dimension'`
|
|
@@ -1779,7 +2163,13 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1779
2163
|
const now = Date.now();
|
|
1780
2164
|
let deletedEntries = 0;
|
|
1781
2165
|
let deletedTasks = 0;
|
|
2166
|
+
const deletedEntryIds = [];
|
|
1782
2167
|
if (params.clearAll) {
|
|
2168
|
+
const entriesToDelete = await this.db.getAllAsync(
|
|
2169
|
+
`SELECT id FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`,
|
|
2170
|
+
[entityId]
|
|
2171
|
+
);
|
|
2172
|
+
deletedEntryIds.push(...entriesToDelete.map((e) => e.id));
|
|
1783
2173
|
const [entriesRes, tasksRes] = await Promise.all([
|
|
1784
2174
|
this.db.runAsync(`UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE entity_id = ? AND deleted_at IS NULL`, [now, now, entityId]),
|
|
1785
2175
|
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 +2187,27 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1797
2187
|
if (params.sourceRef !== void 0 && !sourceRef) throw new Error("Invalid sourceRef");
|
|
1798
2188
|
const sourceHash = params.sourceHash !== void 0 ? normalizeSourceHash(params.sourceHash) : null;
|
|
1799
2189
|
if (params.sourceHash !== void 0 && !sourceHash) throw new Error("Invalid sourceHash (must be 64-char hex string)");
|
|
2190
|
+
if (params.entryId) {
|
|
2191
|
+
const entry = await this.db.getFirstAsync(
|
|
2192
|
+
`SELECT id FROM ${this.prefix}entries WHERE id = ? AND entity_id = ? AND deleted_at IS NULL`,
|
|
2193
|
+
[params.entryId, entityId]
|
|
2194
|
+
);
|
|
2195
|
+
if (entry) deletedEntryIds.push(entry.id);
|
|
2196
|
+
}
|
|
2197
|
+
if (sourceRef || sourceHash) {
|
|
2198
|
+
let q = `SELECT id FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`;
|
|
2199
|
+
const args = [entityId];
|
|
2200
|
+
if (sourceRef) {
|
|
2201
|
+
q += ` AND source_ref = ?`;
|
|
2202
|
+
args.push(sourceRef);
|
|
2203
|
+
}
|
|
2204
|
+
if (sourceHash) {
|
|
2205
|
+
q += ` AND source_hash = ?`;
|
|
2206
|
+
args.push(sourceHash);
|
|
2207
|
+
}
|
|
2208
|
+
const entriesToDelete = await this.db.getAllAsync(q, args);
|
|
2209
|
+
deletedEntryIds.push(...entriesToDelete.map((e) => e.id));
|
|
2210
|
+
}
|
|
1800
2211
|
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
2212
|
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
2213
|
let refPromise = null;
|
|
@@ -1824,6 +2235,14 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1824
2235
|
}
|
|
1825
2236
|
await this.rebuildMiniSearchIndex(entityId);
|
|
1826
2237
|
this.vectorCache.delete(entityId);
|
|
2238
|
+
const uniqueDeletedIds = Array.from(new Set(deletedEntryIds));
|
|
2239
|
+
for (const factId of uniqueDeletedIds) {
|
|
2240
|
+
try {
|
|
2241
|
+
await this._notifyEmbeddingPersisted(entityId, factId, null);
|
|
2242
|
+
} catch (hookErr) {
|
|
2243
|
+
console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during forget for ${factId}:`, hookErr);
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
1827
2246
|
return { deleted: { entries: deletedEntries, tasks: deletedTasks } };
|
|
1828
2247
|
} finally {
|
|
1829
2248
|
this.activeMaintenanceJobs.delete(forgetKey);
|
|
@@ -1893,7 +2312,15 @@ ${chunk}`;
|
|
|
1893
2312
|
}
|
|
1894
2313
|
const now = Date.now();
|
|
1895
2314
|
const insertedFacts = [];
|
|
2315
|
+
const deletedSourceFactIds = [];
|
|
1896
2316
|
await this.db.withTransactionAsync(async () => {
|
|
2317
|
+
const existingSourceFacts = await this.db.getAllAsync(
|
|
2318
|
+
`SELECT id FROM ${this.prefix}entries WHERE source_ref = ? AND entity_id = ? AND deleted_at IS NULL`,
|
|
2319
|
+
[sourceRef, entityId]
|
|
2320
|
+
);
|
|
2321
|
+
for (const row of existingSourceFacts) {
|
|
2322
|
+
deletedSourceFactIds.push(row.id);
|
|
2323
|
+
}
|
|
1897
2324
|
await this.db.runAsync(
|
|
1898
2325
|
`UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE source_ref = ? AND entity_id = ? AND deleted_at IS NULL`,
|
|
1899
2326
|
[now, now, sourceRef, entityId]
|
|
@@ -1905,11 +2332,19 @@ ${chunk}`;
|
|
|
1905
2332
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1906
2333
|
[id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "user_document", sourceHash, sourceRef, now, now]
|
|
1907
2334
|
);
|
|
1908
|
-
insertedFacts.push({ id, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
2335
|
+
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
1909
2336
|
}
|
|
1910
2337
|
});
|
|
1911
2338
|
await this.rebuildMiniSearchIndex(entityId);
|
|
1912
2339
|
this.vectorCache.delete(entityId);
|
|
2340
|
+
const uniqueDeletedSourceFactIds = Array.from(new Set(deletedSourceFactIds));
|
|
2341
|
+
for (const factId of uniqueDeletedSourceFactIds) {
|
|
2342
|
+
try {
|
|
2343
|
+
await this._notifyEmbeddingPersisted(entityId, factId, null);
|
|
2344
|
+
} catch (hookErr) {
|
|
2345
|
+
console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during ingest for ${factId}:`, hookErr);
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
1913
2348
|
for (const fact of insertedFacts) {
|
|
1914
2349
|
await this.embedFact(fact);
|
|
1915
2350
|
}
|