@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/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
- await this.options.vectorRanker?.onEmbeddingPersisted?.({ 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
+ }
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
- deletedEntryIds.push(...entriesToDelete.map((e) => e.id));
771
- const entryResult = await this.db.runAsync(
772
- `DELETE FROM ${this.prefix}entries
773
- WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at < ?`,
774
- [entityId, cutoff]
775
- );
776
- deletedEntries = entryResult.changes;
830
+ const succeeded = [];
831
+ let failure = null;
832
+ for (const row of entriesToDelete) {
833
+ try {
834
+ await this._notifyEmbeddingPersistedOrThrow(row.entity_id, row.id, null);
835
+ succeeded.push({ entity_id: row.entity_id, id: row.id });
836
+ } catch (err) {
837
+ failure = { factId: row.id, cause: err };
838
+ break;
839
+ }
840
+ }
841
+ if (succeeded.length > 0) {
842
+ const chunkSize = 500;
843
+ for (let i = 0; i < succeeded.length; i += chunkSize) {
844
+ const chunk = succeeded.slice(i, i + chunkSize);
845
+ const placeholders = chunk.map(() => "?").join(",");
846
+ const entryResult = await this.db.runAsync(
847
+ `DELETE FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at < ? AND id IN (${placeholders})`,
848
+ [entityId, cutoff, ...chunk.map((r) => r.id)]
849
+ );
850
+ deletedEntries += entryResult.changes;
851
+ }
852
+ }
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?.({ error: rankerError, policy });
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
- mirrored.cause = rankerError;
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 { entityId, queryVec, candidateRows, weight, miniSearchScores, populateCache, limit } = args;
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, queryVec, candidateIds, weight, miniSearchScores, limit } = args;
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 entriesToDelete = await this.db.getAllAsync(
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
- deletedEntryIds.push(...entriesToDelete.map((e) => e.id));
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 = ? AND deleted_at IS NULL`,
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 = ? AND deleted_at IS NULL`;
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._notifyEmbeddingPersisted(entityId, factId, null);
2366
+ await this._notifyEmbeddingPersistedOrThrow(entityId, factId, null);
2242
2367
  } catch (hookErr) {
2243
- console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during forget for ${factId}:`, 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
+ );
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