@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/README.md +192 -1
- package/dist/index.d.mts +164 -7
- package/dist/index.d.ts +164 -7
- package/dist/index.js +664 -86
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +664 -87
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
|
757
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|
-
|
|
1221
|
+
scored = [];
|
|
909
1222
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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.
|
|
928
|
-
|
|
929
|
-
|
|
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
|
|
950
|
-
|
|
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
|
|
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)
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|