@equationalapplications/core-llm-wiki 3.1.0 → 4.0.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 +71 -2
- package/dist/index.d.ts +71 -2
- package/dist/index.js +240 -50
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +239 -51
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -10,7 +10,7 @@ async function setupDatabase(db, prefix) {
|
|
|
10
10
|
body TEXT NOT NULL,
|
|
11
11
|
tags TEXT NOT NULL DEFAULT '[]',
|
|
12
12
|
confidence TEXT NOT NULL DEFAULT 'inferred',
|
|
13
|
-
source_type TEXT NOT NULL DEFAULT '
|
|
13
|
+
source_type TEXT NOT NULL DEFAULT 'librarian_inferred',
|
|
14
14
|
source_hash TEXT,
|
|
15
15
|
source_ref TEXT,
|
|
16
16
|
created_at INTEGER NOT NULL,
|
|
@@ -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("[");
|
|
@@ -574,8 +587,94 @@ var _WikiMemory = class _WikiMemory {
|
|
|
574
587
|
_warnCrossEntityCollision(type, id, existingEntityId, targetEntityId) {
|
|
575
588
|
console.warn(`[WikiMemory] importDump: ${type} id "${id}" already belongs to entity "${existingEntityId}"; skipping for entity "${targetEntityId}"`);
|
|
576
589
|
}
|
|
590
|
+
/** Maps pre-rename enum strings from older dumps to current source_type values. */
|
|
591
|
+
_normalizeImportedSourceType(raw, ctx) {
|
|
592
|
+
if (raw === "user_document") return "immutable_document";
|
|
593
|
+
if (raw === "agent_inferred") return "librarian_inferred";
|
|
594
|
+
const allowed = ["user_stated", "librarian_inferred", "user_confirmed", "immutable_document"];
|
|
595
|
+
if (allowed.includes(raw)) return raw;
|
|
596
|
+
const where = ctx !== void 0 ? ` for entity "${ctx.entityId}" fact "${ctx.factId}"` : "";
|
|
597
|
+
throw new Error(
|
|
598
|
+
`importDump: invalid source_type "${raw}"${where} (expected one of: ${allowed.join(", ")}, or legacy aliases user_document / agent_inferred)`
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
async assertNoLegacySourceTypes() {
|
|
602
|
+
const legacyProbe = await this.db.getFirstAsync(
|
|
603
|
+
`SELECT 1 AS one FROM ${this.prefix}entries
|
|
604
|
+
WHERE source_type IN ('user_document', 'agent_inferred')
|
|
605
|
+
LIMIT 1`,
|
|
606
|
+
[]
|
|
607
|
+
);
|
|
608
|
+
if (!legacyProbe) return;
|
|
609
|
+
const legacyCount = await this.db.getFirstAsync(
|
|
610
|
+
`SELECT COUNT(*) as count FROM ${this.prefix}entries
|
|
611
|
+
WHERE source_type IN ('user_document', 'agent_inferred')`,
|
|
612
|
+
[]
|
|
613
|
+
);
|
|
614
|
+
const count = legacyCount?.count ?? 0;
|
|
615
|
+
const migrationSQL = `
|
|
616
|
+
-- Migrate legacy source_type values (targets your WikiMemory prefix: ${this.prefix})
|
|
617
|
+
UPDATE ${this.prefix}entries SET source_type = 'immutable_document' WHERE source_type = 'user_document';
|
|
618
|
+
UPDATE ${this.prefix}entries SET source_type = 'librarian_inferred' WHERE source_type = 'agent_inferred';
|
|
619
|
+
`.trim();
|
|
620
|
+
throw new Error(
|
|
621
|
+
`Database contains ${count} entries with legacy source_type values ('user_document' or 'agent_inferred'). These enum values were renamed in this release. Running without migration would allow legacy 'user_document' facts to bypass immutability guards, causing data corruption.
|
|
622
|
+
|
|
623
|
+
${migrationSQL}
|
|
624
|
+
|
|
625
|
+
After running the migration SQL, restart your application.`
|
|
626
|
+
);
|
|
627
|
+
}
|
|
577
628
|
async _notifyEmbeddingPersisted(entityId, factId, vector) {
|
|
578
|
-
|
|
629
|
+
if (!this.options.vectorRanker?.onEmbeddingPersisted) return;
|
|
630
|
+
const vectorCopy = vector ? vector.slice() : null;
|
|
631
|
+
await this.options.vectorRanker.onEmbeddingPersisted({
|
|
632
|
+
entityId,
|
|
633
|
+
factId,
|
|
634
|
+
vector: vectorCopy
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* GDPR-critical variant: awaits the hook with a timeout and rethrows failures.
|
|
639
|
+
* Use ONLY on deletion paths. forget() calls after soft-delete UPDATE; runPrune()
|
|
640
|
+
* calls before hard DELETE. For best-effort sync, use _notifyEmbeddingPersisted.
|
|
641
|
+
*/
|
|
642
|
+
async _notifyEmbeddingPersistedOrThrow(entityId, factId, vector) {
|
|
643
|
+
if (!this.options.vectorRanker?.onEmbeddingPersisted) return;
|
|
644
|
+
if (this.options.forceDeleteIgnoreRankerHook === true) return;
|
|
645
|
+
const vectorCopy = vector ? vector.slice() : null;
|
|
646
|
+
const rawTimeout = this.options.deletionHookTimeoutMs ?? 3e4;
|
|
647
|
+
if (typeof rawTimeout !== "number" || !Number.isFinite(rawTimeout) || rawTimeout <= 0) {
|
|
648
|
+
throw new Error("Invalid deletionHookTimeoutMs: must be a positive finite number");
|
|
649
|
+
}
|
|
650
|
+
const timeoutMs = rawTimeout;
|
|
651
|
+
let timeoutHandle;
|
|
652
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
653
|
+
timeoutHandle = setTimeout(
|
|
654
|
+
() => {
|
|
655
|
+
const timeoutError = new Error(`onEmbeddingPersisted timed out after ${timeoutMs}ms`);
|
|
656
|
+
timeoutError[HOOK_TIMEOUT_MARKER] = true;
|
|
657
|
+
reject(timeoutError);
|
|
658
|
+
},
|
|
659
|
+
timeoutMs
|
|
660
|
+
);
|
|
661
|
+
});
|
|
662
|
+
const hookPromise = Promise.resolve(
|
|
663
|
+
this.options.vectorRanker.onEmbeddingPersisted({
|
|
664
|
+
entityId,
|
|
665
|
+
factId,
|
|
666
|
+
vector: vectorCopy
|
|
667
|
+
})
|
|
668
|
+
);
|
|
669
|
+
try {
|
|
670
|
+
await Promise.race([hookPromise, timeoutPromise]);
|
|
671
|
+
} catch (err) {
|
|
672
|
+
hookPromise.catch(() => {
|
|
673
|
+
});
|
|
674
|
+
throw err;
|
|
675
|
+
} finally {
|
|
676
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
677
|
+
}
|
|
579
678
|
}
|
|
580
679
|
async setup() {
|
|
581
680
|
const entriesExistedBeforeSetup = await this.db.getFirstAsync(
|
|
@@ -627,6 +726,9 @@ var _WikiMemory = class _WikiMemory {
|
|
|
627
726
|
);
|
|
628
727
|
}
|
|
629
728
|
}
|
|
729
|
+
if (entriesExistedBeforeSetup) {
|
|
730
|
+
await this.assertNoLegacySourceTypes();
|
|
731
|
+
}
|
|
630
732
|
const rows = await this.db.getAllAsync(`
|
|
631
733
|
SELECT rowid, source_ref FROM ${this.prefix}entries
|
|
632
734
|
WHERE source_ref IS NOT NULL
|
|
@@ -759,33 +861,77 @@ var _WikiMemory = class _WikiMemory {
|
|
|
759
861
|
let deletedEntries = 0;
|
|
760
862
|
let deletedTasks = 0;
|
|
761
863
|
let deletedEvents = 0;
|
|
762
|
-
const deletedEntryIds = [];
|
|
763
864
|
if (retainSoftDeletedFor !== null) {
|
|
764
865
|
const cutoff = now - retainSoftDeletedFor * 864e5;
|
|
765
866
|
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
|
|
867
|
+
`SELECT id, entity_id FROM ${this.prefix}entries
|
|
868
|
+
WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at <= ?`,
|
|
768
869
|
[entityId, cutoff]
|
|
769
870
|
);
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
871
|
+
const succeeded = [];
|
|
872
|
+
let failure = null;
|
|
873
|
+
for (const row of entriesToDelete) {
|
|
874
|
+
try {
|
|
875
|
+
await this._notifyEmbeddingPersistedOrThrow(row.entity_id, row.id, null);
|
|
876
|
+
succeeded.push({ entity_id: row.entity_id, id: row.id });
|
|
877
|
+
} catch (err) {
|
|
878
|
+
failure = { factId: row.id, cause: err };
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
if (succeeded.length > 0) {
|
|
883
|
+
const chunkSize = 500;
|
|
884
|
+
for (let i = 0; i < succeeded.length; i += chunkSize) {
|
|
885
|
+
const chunk = succeeded.slice(i, i + chunkSize);
|
|
886
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
887
|
+
const entryResult = await this.db.runAsync(
|
|
888
|
+
`DELETE FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at <= ? AND id IN (${placeholders})`,
|
|
889
|
+
[entityId, cutoff, ...chunk.map((r) => r.id)]
|
|
890
|
+
);
|
|
891
|
+
deletedEntries += entryResult.changes;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
777
894
|
const taskResult = await this.db.runAsync(
|
|
778
895
|
`DELETE FROM ${this.prefix}tasks
|
|
779
|
-
WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at
|
|
896
|
+
WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at <= ?`,
|
|
780
897
|
[entityId, cutoff]
|
|
781
898
|
);
|
|
782
899
|
deletedTasks = taskResult.changes;
|
|
900
|
+
if (failure) {
|
|
901
|
+
await this.rebuildMiniSearchIndex(entityId);
|
|
902
|
+
this.vectorCache.delete(entityId);
|
|
903
|
+
const remaining = entriesToDelete.length - succeeded.length - 1;
|
|
904
|
+
const isTimeout = failure.cause?.[HOOK_TIMEOUT_MARKER] === true;
|
|
905
|
+
if (isTimeout) {
|
|
906
|
+
throw new PrunePartialFailureError(
|
|
907
|
+
succeeded.length,
|
|
908
|
+
failure.factId,
|
|
909
|
+
remaining,
|
|
910
|
+
new Error("Deletion hook timed out"),
|
|
911
|
+
deletedTasks,
|
|
912
|
+
0
|
|
913
|
+
// events not yet deleted at this point
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
const errMsg = failure.cause?.message ?? "";
|
|
917
|
+
const isValidationError = errMsg.startsWith("Invalid deletionHookTimeoutMs");
|
|
918
|
+
const sanitizedCause = isValidationError ? failure.cause : this._sanitizeRankerError(failure.cause);
|
|
919
|
+
throw new PrunePartialFailureError(
|
|
920
|
+
succeeded.length,
|
|
921
|
+
failure.factId,
|
|
922
|
+
remaining,
|
|
923
|
+
sanitizedCause,
|
|
924
|
+
deletedTasks,
|
|
925
|
+
0
|
|
926
|
+
// events not yet deleted at this point
|
|
927
|
+
);
|
|
928
|
+
}
|
|
783
929
|
}
|
|
784
930
|
if (retainEventsFor !== null) {
|
|
785
931
|
const cutoff = now - retainEventsFor * 864e5;
|
|
786
932
|
const eventResult = await this.db.runAsync(
|
|
787
933
|
`DELETE FROM ${this.prefix}events
|
|
788
|
-
WHERE entity_id = ? AND created_at
|
|
934
|
+
WHERE entity_id = ? AND created_at <= ?`,
|
|
789
935
|
[entityId, cutoff]
|
|
790
936
|
);
|
|
791
937
|
deletedEvents = eventResult.changes;
|
|
@@ -796,14 +942,6 @@ var _WikiMemory = class _WikiMemory {
|
|
|
796
942
|
}
|
|
797
943
|
await this.rebuildMiniSearchIndex(entityId);
|
|
798
944
|
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
945
|
return { entries: deletedEntries, tasks: deletedTasks, events: deletedEvents };
|
|
808
946
|
} finally {
|
|
809
947
|
this.activeMaintenanceJobs.delete(pruneKey);
|
|
@@ -1057,7 +1195,10 @@ var _WikiMemory = class _WikiMemory {
|
|
|
1057
1195
|
} catch (rankerErr) {
|
|
1058
1196
|
const rankerError = rankerErr instanceof Error ? rankerErr : new Error(String(rankerErr));
|
|
1059
1197
|
const policy = this.options.vectorRankerFallback ?? "js-cosine";
|
|
1060
|
-
this.options.onVectorRankerFallback?.({
|
|
1198
|
+
this.options.onVectorRankerFallback?.({
|
|
1199
|
+
error: this._sanitizeRankerError(rankerError),
|
|
1200
|
+
policy
|
|
1201
|
+
});
|
|
1061
1202
|
if (policy === "throw") {
|
|
1062
1203
|
rankerShouldRethrow = true;
|
|
1063
1204
|
throw rankerError;
|
|
@@ -1121,8 +1262,9 @@ var _WikiMemory = class _WikiMemory {
|
|
|
1121
1262
|
scored = [];
|
|
1122
1263
|
}
|
|
1123
1264
|
if (this.options.propagateRankerFailureToRetrievalFallback) {
|
|
1124
|
-
const mirrored = new Error("Vector ranker failed, falling back"
|
|
1125
|
-
|
|
1265
|
+
const mirrored = new Error("Vector ranker failed, falling back", {
|
|
1266
|
+
cause: this._sanitizeRankerError(rankerErr)
|
|
1267
|
+
});
|
|
1126
1268
|
pendingRankerFallbackError = mirrored;
|
|
1127
1269
|
}
|
|
1128
1270
|
}
|
|
@@ -1282,12 +1424,31 @@ var _WikiMemory = class _WikiMemory {
|
|
|
1282
1424
|
if (updatedAtDiff !== 0) return updatedAtDiff;
|
|
1283
1425
|
return a.id.localeCompare(b.id);
|
|
1284
1426
|
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Strip potentially sensitive data from ranker errors before exposing to host callbacks.
|
|
1429
|
+
* Preserves error type for debugging but removes message/stack that may contain credentials.
|
|
1430
|
+
* Recursively sanitizes one level of .cause; deeper chains collapse to type only.
|
|
1431
|
+
*/
|
|
1432
|
+
_sanitizeRankerError(err) {
|
|
1433
|
+
if (this.options.sanitizeRankerErrors === false) {
|
|
1434
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
1435
|
+
}
|
|
1436
|
+
const typeName = err instanceof Error ? err.constructor?.name ?? "Error" : typeof err;
|
|
1437
|
+
const innerCause = err instanceof Error && err.cause !== void 0 ? new Error(`Caused by: ${err.cause?.constructor?.name ?? typeof err.cause}`) : void 0;
|
|
1438
|
+
const sanitized = new Error(
|
|
1439
|
+
`VectorRanker ${typeName} (message scrubbed for security)`,
|
|
1440
|
+
innerCause ? { cause: innerCause } : void 0
|
|
1441
|
+
);
|
|
1442
|
+
sanitized.name = typeName;
|
|
1443
|
+
return sanitized;
|
|
1444
|
+
}
|
|
1285
1445
|
/**
|
|
1286
1446
|
* Score candidate rows using in-process JS cosine similarity.
|
|
1287
1447
|
* Applies hybrid blending (if weight set) and tie-break sorting before returning.
|
|
1288
1448
|
*/
|
|
1289
1449
|
async _rankWithJsCosine(args) {
|
|
1290
|
-
const
|
|
1450
|
+
const queryVec = args.queryVec instanceof Float32Array ? args.queryVec.slice() : Array.from(args.queryVec);
|
|
1451
|
+
const { entityId, candidateRows, weight, miniSearchScores, populateCache, limit } = args;
|
|
1291
1452
|
let entityCache = this.vectorCache.get(entityId);
|
|
1292
1453
|
const tooLarge = populateCache && candidateRows.length > _WikiMemory.MAX_VECTOR_CACHE_FACTS_PER_ENTITY;
|
|
1293
1454
|
if (tooLarge && entityCache) {
|
|
@@ -1338,14 +1499,15 @@ var _WikiMemory = class _WikiMemory {
|
|
|
1338
1499
|
* Returns scored results ready for hybrid blending and tie-break sorting.
|
|
1339
1500
|
*/
|
|
1340
1501
|
async _rankWithVectorRanker(args) {
|
|
1341
|
-
const { entityId,
|
|
1502
|
+
const { entityId, candidateIds, weight, miniSearchScores, limit } = args;
|
|
1342
1503
|
const ranker = this.options.vectorRanker;
|
|
1343
1504
|
if (!ranker) {
|
|
1344
1505
|
throw new Error("vectorRanker not configured");
|
|
1345
1506
|
}
|
|
1507
|
+
const queryVecCopy = args.queryVec instanceof Float32Array ? args.queryVec.slice() : Array.from(args.queryVec);
|
|
1346
1508
|
const rankerResults = await ranker.rankBySimilarity({
|
|
1347
1509
|
entityId,
|
|
1348
|
-
queryVec,
|
|
1510
|
+
queryVec: queryVecCopy,
|
|
1349
1511
|
candidateIds,
|
|
1350
1512
|
limit
|
|
1351
1513
|
});
|
|
@@ -1470,7 +1632,7 @@ ${JSON.stringify(currentFacts, null, 2)}`;
|
|
|
1470
1632
|
let skip = false;
|
|
1471
1633
|
if (newTokens.size >= MIN_TOKENS_TO_QUALIFY) {
|
|
1472
1634
|
for (const existing of currentFactsRows) {
|
|
1473
|
-
if (existing.source_type !== "
|
|
1635
|
+
if (existing.source_type !== "librarian_inferred") continue;
|
|
1474
1636
|
const existingTokens = titleTokens(existing.title);
|
|
1475
1637
|
if (existingTokens.size >= MIN_TOKENS_TO_QUALIFY) {
|
|
1476
1638
|
if (jaccardScore(newTokens, existingTokens) >= FUZZY_THRESHOLD) {
|
|
@@ -1485,7 +1647,7 @@ ${JSON.stringify(currentFacts, null, 2)}`;
|
|
|
1485
1647
|
await this.db.runAsync(`
|
|
1486
1648
|
INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, created_at, updated_at)
|
|
1487
1649
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1488
|
-
`, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "
|
|
1650
|
+
`, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "librarian_inferred", now, now]);
|
|
1489
1651
|
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
1490
1652
|
}
|
|
1491
1653
|
for (const task of validTasks) {
|
|
@@ -1518,25 +1680,25 @@ ${JSON.stringify(currentFacts, null, 2)}`;
|
|
|
1518
1680
|
if (orphanAfterDays !== null) {
|
|
1519
1681
|
const orphanThreshold = now - orphanAfterDays * MS_PER_DAY;
|
|
1520
1682
|
await this.db.runAsync(`
|
|
1521
|
-
UPDATE ${this.prefix}entries
|
|
1522
|
-
SET deleted_at = ?, updated_at = ?
|
|
1523
|
-
WHERE entity_id = ? AND access_count = 0 AND created_at
|
|
1683
|
+
UPDATE ${this.prefix}entries
|
|
1684
|
+
SET deleted_at = ?, updated_at = ?
|
|
1685
|
+
WHERE entity_id = ? AND access_count = 0 AND created_at <= ? AND source_type != 'immutable_document' AND deleted_at IS NULL
|
|
1524
1686
|
`, [now, now, entityId, orphanThreshold]);
|
|
1525
1687
|
}
|
|
1526
1688
|
if (staleInferredAfterDays !== null) {
|
|
1527
1689
|
const staleThreshold = now - staleInferredAfterDays * MS_PER_DAY;
|
|
1528
1690
|
await this.db.runAsync(`
|
|
1529
|
-
UPDATE ${this.prefix}entries
|
|
1530
|
-
SET confidence = 'tentative', updated_at = ?
|
|
1531
|
-
WHERE entity_id = ? AND confidence = 'inferred' AND (last_accessed_at
|
|
1691
|
+
UPDATE ${this.prefix}entries
|
|
1692
|
+
SET confidence = 'tentative', updated_at = ?
|
|
1693
|
+
WHERE entity_id = ? AND confidence = 'inferred' AND (last_accessed_at <= ? OR (last_accessed_at IS NULL AND created_at <= ?)) AND source_type != 'immutable_document' AND deleted_at IS NULL
|
|
1532
1694
|
`, [now, entityId, staleThreshold, staleThreshold]);
|
|
1533
1695
|
}
|
|
1534
1696
|
});
|
|
1535
1697
|
const allFactsRows = await this.db.getAllAsync(`SELECT * FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`, [entityId]);
|
|
1536
1698
|
const allTasks = await this.db.getAllAsync(`SELECT * FROM ${this.prefix}tasks WHERE entity_id = ? AND status IN ('pending', 'in_progress') AND deleted_at IS NULL`, [entityId]);
|
|
1537
1699
|
const recentEvents = await this.db.getAllAsync(`SELECT * FROM ${this.prefix}events WHERE entity_id = ? ORDER BY created_at DESC LIMIT 20`, [entityId]);
|
|
1538
|
-
const healCandidates = allFactsRows.filter((f) => f.source_type !== "
|
|
1539
|
-
const documentAnchors = allFactsRows.filter((f) => f.source_type === "
|
|
1700
|
+
const healCandidates = allFactsRows.filter((f) => f.source_type !== "immutable_document");
|
|
1701
|
+
const documentAnchors = allFactsRows.filter((f) => f.source_type === "immutable_document").map(({ id, title, source_ref }) => ({ id, title, source_ref }));
|
|
1540
1702
|
const userPrompt = `Heal Candidates:
|
|
1541
1703
|
${JSON.stringify(healCandidates.map((f) => {
|
|
1542
1704
|
const { embedding: _embedding, embedding_blob: _blob, ...rest } = f;
|
|
@@ -1579,7 +1741,7 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1579
1741
|
await this.db.runAsync(`
|
|
1580
1742
|
INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, created_at, updated_at)
|
|
1581
1743
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1582
|
-
`, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "
|
|
1744
|
+
`, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "librarian_inferred", now, now]);
|
|
1583
1745
|
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
1584
1746
|
}
|
|
1585
1747
|
});
|
|
@@ -1879,6 +2041,7 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1879
2041
|
this.activeMaintenanceJobs.add(this._importKey(entityId));
|
|
1880
2042
|
}
|
|
1881
2043
|
try {
|
|
2044
|
+
await this.assertNoLegacySourceTypes();
|
|
1882
2045
|
for (const [entityId, bundle] of Object.entries(dump.entities)) {
|
|
1883
2046
|
await this._doImportEntity(entityId, bundle, merge);
|
|
1884
2047
|
}
|
|
@@ -1932,6 +2095,10 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1932
2095
|
}
|
|
1933
2096
|
}
|
|
1934
2097
|
for (const fact of bundle.facts) {
|
|
2098
|
+
const sourceType = this._normalizeImportedSourceType(String(fact.source_type), {
|
|
2099
|
+
entityId,
|
|
2100
|
+
factId: fact.id
|
|
2101
|
+
});
|
|
1935
2102
|
const tagsJson = JSON.stringify(Array.isArray(fact.tags) ? fact.tags : []);
|
|
1936
2103
|
const safeUpdatedAt = Number.isFinite(fact.updated_at) ? fact.updated_at : 0;
|
|
1937
2104
|
const existing = existingFactsById.get(fact.id);
|
|
@@ -1980,14 +2147,14 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1980
2147
|
if (blobData != null) {
|
|
1981
2148
|
await this.db.runAsync(
|
|
1982
2149
|
`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 = ?`,
|
|
1983
|
-
[entityId, fact.title, fact.body, tagsJson, fact.confidence,
|
|
2150
|
+
[entityId, fact.title, fact.body, tagsJson, fact.confidence, sourceType, fact.source_hash, fact.source_ref, fact.created_at, safeUpdatedAt, fact.last_accessed_at, fact.access_count, fact.deleted_at, blobData, fact.id]
|
|
1984
2151
|
);
|
|
1985
2152
|
factsWithPreservedBlob.set(fact.id, blobData);
|
|
1986
2153
|
if (!fact.deleted_at) preservedBlobDims.add(blobData.byteLength / 4);
|
|
1987
2154
|
} else {
|
|
1988
2155
|
await this.db.runAsync(
|
|
1989
2156
|
`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 = NULL, embedding = NULL WHERE id = ?`,
|
|
1990
|
-
[entityId, fact.title, fact.body, tagsJson, fact.confidence,
|
|
2157
|
+
[entityId, fact.title, fact.body, tagsJson, fact.confidence, sourceType, fact.source_hash, fact.source_ref, fact.created_at, safeUpdatedAt, fact.last_accessed_at, fact.access_count, fact.deleted_at, fact.id]
|
|
1991
2158
|
);
|
|
1992
2159
|
}
|
|
1993
2160
|
existingFactsById.set(fact.id, { id: fact.id, entity_id: entityId, updated_at: safeUpdatedAt });
|
|
@@ -1997,14 +2164,14 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1997
2164
|
if (blobData != null) {
|
|
1998
2165
|
await this.db.runAsync(
|
|
1999
2166
|
`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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2000
|
-
[fact.id, entityId, fact.title, fact.body, tagsJson, fact.confidence,
|
|
2167
|
+
[fact.id, entityId, fact.title, fact.body, tagsJson, fact.confidence, sourceType, fact.source_hash, fact.source_ref, fact.created_at, safeUpdatedAt, fact.last_accessed_at, fact.access_count, fact.deleted_at, blobData]
|
|
2001
2168
|
);
|
|
2002
2169
|
factsWithPreservedBlob.set(fact.id, blobData);
|
|
2003
2170
|
if (!fact.deleted_at) preservedBlobDims.add(blobData.byteLength / 4);
|
|
2004
2171
|
} else {
|
|
2005
2172
|
await this.db.runAsync(
|
|
2006
2173
|
`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) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2007
|
-
[fact.id, entityId, fact.title, fact.body, tagsJson, fact.confidence,
|
|
2174
|
+
[fact.id, entityId, fact.title, fact.body, tagsJson, fact.confidence, sourceType, fact.source_hash, fact.source_ref, fact.created_at, safeUpdatedAt, fact.last_accessed_at, fact.access_count, fact.deleted_at]
|
|
2008
2175
|
);
|
|
2009
2176
|
}
|
|
2010
2177
|
existingFactsById.set(fact.id, { id: fact.id, entity_id: entityId, updated_at: safeUpdatedAt });
|
|
@@ -2165,11 +2332,15 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
2165
2332
|
let deletedTasks = 0;
|
|
2166
2333
|
const deletedEntryIds = [];
|
|
2167
2334
|
if (params.clearAll) {
|
|
2168
|
-
const
|
|
2335
|
+
const newDeletions = await this.db.getAllAsync(
|
|
2169
2336
|
`SELECT id FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`,
|
|
2170
2337
|
[entityId]
|
|
2171
2338
|
);
|
|
2172
|
-
|
|
2339
|
+
const alreadySoftDeleted = await this.db.getAllAsync(
|
|
2340
|
+
`SELECT id FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NOT NULL`,
|
|
2341
|
+
[entityId]
|
|
2342
|
+
);
|
|
2343
|
+
deletedEntryIds.push(...newDeletions.map((e) => e.id), ...alreadySoftDeleted.map((e) => e.id));
|
|
2173
2344
|
const [entriesRes, tasksRes] = await Promise.all([
|
|
2174
2345
|
this.db.runAsync(`UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE entity_id = ? AND deleted_at IS NULL`, [now, now, entityId]),
|
|
2175
2346
|
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 +2360,13 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
2189
2360
|
if (params.sourceHash !== void 0 && !sourceHash) throw new Error("Invalid sourceHash (must be 64-char hex string)");
|
|
2190
2361
|
if (params.entryId) {
|
|
2191
2362
|
const entry = await this.db.getFirstAsync(
|
|
2192
|
-
`SELECT id FROM ${this.prefix}entries WHERE id = ? AND entity_id =
|
|
2363
|
+
`SELECT id FROM ${this.prefix}entries WHERE id = ? AND entity_id = ?`,
|
|
2193
2364
|
[params.entryId, entityId]
|
|
2194
2365
|
);
|
|
2195
2366
|
if (entry) deletedEntryIds.push(entry.id);
|
|
2196
2367
|
}
|
|
2197
2368
|
if (sourceRef || sourceHash) {
|
|
2198
|
-
let q = `SELECT id FROM ${this.prefix}entries WHERE entity_id =
|
|
2369
|
+
let q = `SELECT id FROM ${this.prefix}entries WHERE entity_id = ?`;
|
|
2199
2370
|
const args = [entityId];
|
|
2200
2371
|
if (sourceRef) {
|
|
2201
2372
|
q += ` AND source_ref = ?`;
|
|
@@ -2238,9 +2409,26 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
2238
2409
|
const uniqueDeletedIds = Array.from(new Set(deletedEntryIds));
|
|
2239
2410
|
for (const factId of uniqueDeletedIds) {
|
|
2240
2411
|
try {
|
|
2241
|
-
await this.
|
|
2412
|
+
await this._notifyEmbeddingPersistedOrThrow(entityId, factId, null);
|
|
2242
2413
|
} catch (hookErr) {
|
|
2243
|
-
|
|
2414
|
+
const isTimeout = hookErr?.[HOOK_TIMEOUT_MARKER] === true;
|
|
2415
|
+
if (isTimeout) {
|
|
2416
|
+
throw new Error(
|
|
2417
|
+
`forget(${entityId}/${factId}) failed: ${hookErr.message}`
|
|
2418
|
+
);
|
|
2419
|
+
}
|
|
2420
|
+
const errMsg = hookErr?.message ?? "";
|
|
2421
|
+
const isValidationError = errMsg.startsWith("Invalid deletionHookTimeoutMs");
|
|
2422
|
+
if (isValidationError) {
|
|
2423
|
+
throw new Error(
|
|
2424
|
+
`forget(${entityId}/${factId}) failed: ${errMsg}`,
|
|
2425
|
+
{ cause: hookErr }
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
throw new Error(
|
|
2429
|
+
`forget(${entityId}/${factId}) failed: ANN cleanup hook rejected`,
|
|
2430
|
+
{ cause: this._sanitizeRankerError(hookErr) }
|
|
2431
|
+
);
|
|
2244
2432
|
}
|
|
2245
2433
|
}
|
|
2246
2434
|
return { deleted: { entries: deletedEntries, tasks: deletedTasks } };
|
|
@@ -2330,7 +2518,7 @@ ${chunk}`;
|
|
|
2330
2518
|
await this.db.runAsync(
|
|
2331
2519
|
`INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, source_hash, source_ref, created_at, updated_at)
|
|
2332
2520
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2333
|
-
[id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "
|
|
2521
|
+
[id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "immutable_document", sourceHash, sourceRef, now, now]
|
|
2334
2522
|
);
|
|
2335
2523
|
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
2336
2524
|
}
|
|
@@ -2589,6 +2777,6 @@ function createWiki(db, options) {
|
|
|
2589
2777
|
return new WikiMemory(db, options);
|
|
2590
2778
|
}
|
|
2591
2779
|
|
|
2592
|
-
export { WikiBusyError, WikiMemory, createWiki, formatContext, formatMemoryDump };
|
|
2780
|
+
export { PrunePartialFailureError, WikiBusyError, WikiMemory, createWiki, formatContext, formatMemoryDump, parseEmbedding };
|
|
2593
2781
|
//# sourceMappingURL=index.mjs.map
|
|
2594
2782
|
//# sourceMappingURL=index.mjs.map
|