@equationalapplications/core-llm-wiki 3.1.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 +33 -0
- package/dist/index.d.mts +57 -1
- package/dist/index.d.ts +57 -1
- package/dist/index.js +173 -30
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +173 -31
- 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("[");
|
|
@@ -575,7 +588,55 @@ var _WikiMemory = class _WikiMemory {
|
|
|
575
588
|
console.warn(`[WikiMemory] importDump: ${type} id "${id}" already belongs to entity "${existingEntityId}"; skipping for entity "${targetEntityId}"`);
|
|
576
589
|
}
|
|
577
590
|
async _notifyEmbeddingPersisted(entityId, factId, vector) {
|
|
578
|
-
|
|
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
|
+
}
|
|
579
640
|
}
|
|
580
641
|
async setup() {
|
|
581
642
|
const entriesExistedBeforeSetup = await this.db.getFirstAsync(
|
|
@@ -759,27 +820,71 @@ var _WikiMemory = class _WikiMemory {
|
|
|
759
820
|
let deletedEntries = 0;
|
|
760
821
|
let deletedTasks = 0;
|
|
761
822
|
let deletedEvents = 0;
|
|
762
|
-
const deletedEntryIds = [];
|
|
763
823
|
if (retainSoftDeletedFor !== null) {
|
|
764
824
|
const cutoff = now - retainSoftDeletedFor * 864e5;
|
|
765
825
|
const entriesToDelete = await this.db.getAllAsync(
|
|
766
|
-
`SELECT id FROM ${this.prefix}entries
|
|
826
|
+
`SELECT id, entity_id FROM ${this.prefix}entries
|
|
767
827
|
WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at < ?`,
|
|
768
828
|
[entityId, cutoff]
|
|
769
829
|
);
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
+
}
|
|
777
853
|
const taskResult = await this.db.runAsync(
|
|
778
854
|
`DELETE FROM ${this.prefix}tasks
|
|
779
855
|
WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at < ?`,
|
|
780
856
|
[entityId, cutoff]
|
|
781
857
|
);
|
|
782
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
|
+
}
|
|
783
888
|
}
|
|
784
889
|
if (retainEventsFor !== null) {
|
|
785
890
|
const cutoff = now - retainEventsFor * 864e5;
|
|
@@ -796,14 +901,6 @@ var _WikiMemory = class _WikiMemory {
|
|
|
796
901
|
}
|
|
797
902
|
await this.rebuildMiniSearchIndex(entityId);
|
|
798
903
|
this.vectorCache.delete(entityId);
|
|
799
|
-
const uniqueDeletedIds = Array.from(new Set(deletedEntryIds));
|
|
800
|
-
for (const factId of uniqueDeletedIds) {
|
|
801
|
-
try {
|
|
802
|
-
await this._notifyEmbeddingPersisted(entityId, factId, null);
|
|
803
|
-
} catch (hookErr) {
|
|
804
|
-
console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during prune for ${factId}:`, hookErr);
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
904
|
return { entries: deletedEntries, tasks: deletedTasks, events: deletedEvents };
|
|
808
905
|
} finally {
|
|
809
906
|
this.activeMaintenanceJobs.delete(pruneKey);
|
|
@@ -1057,7 +1154,10 @@ var _WikiMemory = class _WikiMemory {
|
|
|
1057
1154
|
} catch (rankerErr) {
|
|
1058
1155
|
const rankerError = rankerErr instanceof Error ? rankerErr : new Error(String(rankerErr));
|
|
1059
1156
|
const policy = this.options.vectorRankerFallback ?? "js-cosine";
|
|
1060
|
-
this.options.onVectorRankerFallback?.({
|
|
1157
|
+
this.options.onVectorRankerFallback?.({
|
|
1158
|
+
error: this._sanitizeRankerError(rankerError),
|
|
1159
|
+
policy
|
|
1160
|
+
});
|
|
1061
1161
|
if (policy === "throw") {
|
|
1062
1162
|
rankerShouldRethrow = true;
|
|
1063
1163
|
throw rankerError;
|
|
@@ -1121,8 +1221,9 @@ var _WikiMemory = class _WikiMemory {
|
|
|
1121
1221
|
scored = [];
|
|
1122
1222
|
}
|
|
1123
1223
|
if (this.options.propagateRankerFailureToRetrievalFallback) {
|
|
1124
|
-
const mirrored = new Error("Vector ranker failed, falling back"
|
|
1125
|
-
|
|
1224
|
+
const mirrored = new Error("Vector ranker failed, falling back", {
|
|
1225
|
+
cause: this._sanitizeRankerError(rankerErr)
|
|
1226
|
+
});
|
|
1126
1227
|
pendingRankerFallbackError = mirrored;
|
|
1127
1228
|
}
|
|
1128
1229
|
}
|
|
@@ -1282,12 +1383,31 @@ var _WikiMemory = class _WikiMemory {
|
|
|
1282
1383
|
if (updatedAtDiff !== 0) return updatedAtDiff;
|
|
1283
1384
|
return a.id.localeCompare(b.id);
|
|
1284
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
|
+
}
|
|
1285
1404
|
/**
|
|
1286
1405
|
* Score candidate rows using in-process JS cosine similarity.
|
|
1287
1406
|
* Applies hybrid blending (if weight set) and tie-break sorting before returning.
|
|
1288
1407
|
*/
|
|
1289
1408
|
async _rankWithJsCosine(args) {
|
|
1290
|
-
const
|
|
1409
|
+
const queryVec = args.queryVec instanceof Float32Array ? args.queryVec.slice() : Array.from(args.queryVec);
|
|
1410
|
+
const { entityId, candidateRows, weight, miniSearchScores, populateCache, limit } = args;
|
|
1291
1411
|
let entityCache = this.vectorCache.get(entityId);
|
|
1292
1412
|
const tooLarge = populateCache && candidateRows.length > _WikiMemory.MAX_VECTOR_CACHE_FACTS_PER_ENTITY;
|
|
1293
1413
|
if (tooLarge && entityCache) {
|
|
@@ -1338,14 +1458,15 @@ var _WikiMemory = class _WikiMemory {
|
|
|
1338
1458
|
* Returns scored results ready for hybrid blending and tie-break sorting.
|
|
1339
1459
|
*/
|
|
1340
1460
|
async _rankWithVectorRanker(args) {
|
|
1341
|
-
const { entityId,
|
|
1461
|
+
const { entityId, candidateIds, weight, miniSearchScores, limit } = args;
|
|
1342
1462
|
const ranker = this.options.vectorRanker;
|
|
1343
1463
|
if (!ranker) {
|
|
1344
1464
|
throw new Error("vectorRanker not configured");
|
|
1345
1465
|
}
|
|
1466
|
+
const queryVecCopy = args.queryVec instanceof Float32Array ? args.queryVec.slice() : Array.from(args.queryVec);
|
|
1346
1467
|
const rankerResults = await ranker.rankBySimilarity({
|
|
1347
1468
|
entityId,
|
|
1348
|
-
queryVec,
|
|
1469
|
+
queryVec: queryVecCopy,
|
|
1349
1470
|
candidateIds,
|
|
1350
1471
|
limit
|
|
1351
1472
|
});
|
|
@@ -2165,11 +2286,15 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
2165
2286
|
let deletedTasks = 0;
|
|
2166
2287
|
const deletedEntryIds = [];
|
|
2167
2288
|
if (params.clearAll) {
|
|
2168
|
-
const
|
|
2289
|
+
const newDeletions = await this.db.getAllAsync(
|
|
2169
2290
|
`SELECT id FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`,
|
|
2170
2291
|
[entityId]
|
|
2171
2292
|
);
|
|
2172
|
-
|
|
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));
|
|
2173
2298
|
const [entriesRes, tasksRes] = await Promise.all([
|
|
2174
2299
|
this.db.runAsync(`UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE entity_id = ? AND deleted_at IS NULL`, [now, now, entityId]),
|
|
2175
2300
|
this.db.runAsync(`UPDATE ${this.prefix}tasks SET deleted_at = ?, updated_at = ? WHERE entity_id = ? AND deleted_at IS NULL`, [now, now, entityId])
|
|
@@ -2189,13 +2314,13 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
2189
2314
|
if (params.sourceHash !== void 0 && !sourceHash) throw new Error("Invalid sourceHash (must be 64-char hex string)");
|
|
2190
2315
|
if (params.entryId) {
|
|
2191
2316
|
const entry = await this.db.getFirstAsync(
|
|
2192
|
-
`SELECT id FROM ${this.prefix}entries WHERE id = ? AND entity_id =
|
|
2317
|
+
`SELECT id FROM ${this.prefix}entries WHERE id = ? AND entity_id = ?`,
|
|
2193
2318
|
[params.entryId, entityId]
|
|
2194
2319
|
);
|
|
2195
2320
|
if (entry) deletedEntryIds.push(entry.id);
|
|
2196
2321
|
}
|
|
2197
2322
|
if (sourceRef || sourceHash) {
|
|
2198
|
-
let q = `SELECT id FROM ${this.prefix}entries WHERE entity_id =
|
|
2323
|
+
let q = `SELECT id FROM ${this.prefix}entries WHERE entity_id = ?`;
|
|
2199
2324
|
const args = [entityId];
|
|
2200
2325
|
if (sourceRef) {
|
|
2201
2326
|
q += ` AND source_ref = ?`;
|
|
@@ -2238,9 +2363,26 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
2238
2363
|
const uniqueDeletedIds = Array.from(new Set(deletedEntryIds));
|
|
2239
2364
|
for (const factId of uniqueDeletedIds) {
|
|
2240
2365
|
try {
|
|
2241
|
-
await this.
|
|
2366
|
+
await this._notifyEmbeddingPersistedOrThrow(entityId, factId, null);
|
|
2242
2367
|
} catch (hookErr) {
|
|
2243
|
-
|
|
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
|
+
);
|
|
2244
2386
|
}
|
|
2245
2387
|
}
|
|
2246
2388
|
return { deleted: { entries: deletedEntries, tasks: deletedTasks } };
|
|
@@ -2589,6 +2731,6 @@ function createWiki(db, options) {
|
|
|
2589
2731
|
return new WikiMemory(db, options);
|
|
2590
2732
|
}
|
|
2591
2733
|
|
|
2592
|
-
export { WikiBusyError, WikiMemory, createWiki, formatContext, formatMemoryDump };
|
|
2734
|
+
export { PrunePartialFailureError, WikiBusyError, WikiMemory, createWiki, formatContext, formatMemoryDump };
|
|
2593
2735
|
//# sourceMappingURL=index.mjs.map
|
|
2594
2736
|
//# sourceMappingURL=index.mjs.map
|