@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.js
CHANGED
|
@@ -16,7 +16,7 @@ async function setupDatabase(db, prefix) {
|
|
|
16
16
|
body TEXT NOT NULL,
|
|
17
17
|
tags TEXT NOT NULL DEFAULT '[]',
|
|
18
18
|
confidence TEXT NOT NULL DEFAULT 'inferred',
|
|
19
|
-
source_type TEXT NOT NULL DEFAULT '
|
|
19
|
+
source_type TEXT NOT NULL DEFAULT 'librarian_inferred',
|
|
20
20
|
source_hash TEXT,
|
|
21
21
|
source_ref TEXT,
|
|
22
22
|
created_at INTEGER NOT NULL,
|
|
@@ -132,6 +132,18 @@ var WikiBusyError = class extends Error {
|
|
|
132
132
|
this.entityId = entityId;
|
|
133
133
|
}
|
|
134
134
|
};
|
|
135
|
+
var PrunePartialFailureError = class extends Error {
|
|
136
|
+
constructor(deleted, failedAt, remaining, cause, deletedTasks = 0, deletedEvents = 0) {
|
|
137
|
+
super(`Prune partially failed: deleted ${deleted}, failed at ${failedAt}, ${remaining} remaining`);
|
|
138
|
+
this.name = "PrunePartialFailureError";
|
|
139
|
+
this.deleted = deleted;
|
|
140
|
+
this.failedAt = failedAt;
|
|
141
|
+
this.remaining = remaining;
|
|
142
|
+
this.deletedTasks = deletedTasks;
|
|
143
|
+
this.deletedEvents = deletedEvents;
|
|
144
|
+
this.cause = cause;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
135
147
|
|
|
136
148
|
// src/prompts.ts
|
|
137
149
|
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.
|
|
@@ -198,6 +210,7 @@ function parseEmbedding(blob, text) {
|
|
|
198
210
|
}
|
|
199
211
|
|
|
200
212
|
// src/WikiMemory.ts
|
|
213
|
+
var HOOK_TIMEOUT_MARKER = /* @__PURE__ */ Symbol("WikiMemoryHookTimeout");
|
|
201
214
|
function parseJsonResponse(text) {
|
|
202
215
|
const firstBrace = text.indexOf("{");
|
|
203
216
|
const firstBracket = text.indexOf("[");
|
|
@@ -580,8 +593,94 @@ var _WikiMemory = class _WikiMemory {
|
|
|
580
593
|
_warnCrossEntityCollision(type, id, existingEntityId, targetEntityId) {
|
|
581
594
|
console.warn(`[WikiMemory] importDump: ${type} id "${id}" already belongs to entity "${existingEntityId}"; skipping for entity "${targetEntityId}"`);
|
|
582
595
|
}
|
|
596
|
+
/** Maps pre-rename enum strings from older dumps to current source_type values. */
|
|
597
|
+
_normalizeImportedSourceType(raw, ctx) {
|
|
598
|
+
if (raw === "user_document") return "immutable_document";
|
|
599
|
+
if (raw === "agent_inferred") return "librarian_inferred";
|
|
600
|
+
const allowed = ["user_stated", "librarian_inferred", "user_confirmed", "immutable_document"];
|
|
601
|
+
if (allowed.includes(raw)) return raw;
|
|
602
|
+
const where = ctx !== void 0 ? ` for entity "${ctx.entityId}" fact "${ctx.factId}"` : "";
|
|
603
|
+
throw new Error(
|
|
604
|
+
`importDump: invalid source_type "${raw}"${where} (expected one of: ${allowed.join(", ")}, or legacy aliases user_document / agent_inferred)`
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
async assertNoLegacySourceTypes() {
|
|
608
|
+
const legacyProbe = await this.db.getFirstAsync(
|
|
609
|
+
`SELECT 1 AS one FROM ${this.prefix}entries
|
|
610
|
+
WHERE source_type IN ('user_document', 'agent_inferred')
|
|
611
|
+
LIMIT 1`,
|
|
612
|
+
[]
|
|
613
|
+
);
|
|
614
|
+
if (!legacyProbe) return;
|
|
615
|
+
const legacyCount = await this.db.getFirstAsync(
|
|
616
|
+
`SELECT COUNT(*) as count FROM ${this.prefix}entries
|
|
617
|
+
WHERE source_type IN ('user_document', 'agent_inferred')`,
|
|
618
|
+
[]
|
|
619
|
+
);
|
|
620
|
+
const count = legacyCount?.count ?? 0;
|
|
621
|
+
const migrationSQL = `
|
|
622
|
+
-- Migrate legacy source_type values (targets your WikiMemory prefix: ${this.prefix})
|
|
623
|
+
UPDATE ${this.prefix}entries SET source_type = 'immutable_document' WHERE source_type = 'user_document';
|
|
624
|
+
UPDATE ${this.prefix}entries SET source_type = 'librarian_inferred' WHERE source_type = 'agent_inferred';
|
|
625
|
+
`.trim();
|
|
626
|
+
throw new Error(
|
|
627
|
+
`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.
|
|
628
|
+
|
|
629
|
+
${migrationSQL}
|
|
630
|
+
|
|
631
|
+
After running the migration SQL, restart your application.`
|
|
632
|
+
);
|
|
633
|
+
}
|
|
583
634
|
async _notifyEmbeddingPersisted(entityId, factId, vector) {
|
|
584
|
-
|
|
635
|
+
if (!this.options.vectorRanker?.onEmbeddingPersisted) return;
|
|
636
|
+
const vectorCopy = vector ? vector.slice() : null;
|
|
637
|
+
await this.options.vectorRanker.onEmbeddingPersisted({
|
|
638
|
+
entityId,
|
|
639
|
+
factId,
|
|
640
|
+
vector: vectorCopy
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* GDPR-critical variant: awaits the hook with a timeout and rethrows failures.
|
|
645
|
+
* Use ONLY on deletion paths. forget() calls after soft-delete UPDATE; runPrune()
|
|
646
|
+
* calls before hard DELETE. For best-effort sync, use _notifyEmbeddingPersisted.
|
|
647
|
+
*/
|
|
648
|
+
async _notifyEmbeddingPersistedOrThrow(entityId, factId, vector) {
|
|
649
|
+
if (!this.options.vectorRanker?.onEmbeddingPersisted) return;
|
|
650
|
+
if (this.options.forceDeleteIgnoreRankerHook === true) return;
|
|
651
|
+
const vectorCopy = vector ? vector.slice() : null;
|
|
652
|
+
const rawTimeout = this.options.deletionHookTimeoutMs ?? 3e4;
|
|
653
|
+
if (typeof rawTimeout !== "number" || !Number.isFinite(rawTimeout) || rawTimeout <= 0) {
|
|
654
|
+
throw new Error("Invalid deletionHookTimeoutMs: must be a positive finite number");
|
|
655
|
+
}
|
|
656
|
+
const timeoutMs = rawTimeout;
|
|
657
|
+
let timeoutHandle;
|
|
658
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
659
|
+
timeoutHandle = setTimeout(
|
|
660
|
+
() => {
|
|
661
|
+
const timeoutError = new Error(`onEmbeddingPersisted timed out after ${timeoutMs}ms`);
|
|
662
|
+
timeoutError[HOOK_TIMEOUT_MARKER] = true;
|
|
663
|
+
reject(timeoutError);
|
|
664
|
+
},
|
|
665
|
+
timeoutMs
|
|
666
|
+
);
|
|
667
|
+
});
|
|
668
|
+
const hookPromise = Promise.resolve(
|
|
669
|
+
this.options.vectorRanker.onEmbeddingPersisted({
|
|
670
|
+
entityId,
|
|
671
|
+
factId,
|
|
672
|
+
vector: vectorCopy
|
|
673
|
+
})
|
|
674
|
+
);
|
|
675
|
+
try {
|
|
676
|
+
await Promise.race([hookPromise, timeoutPromise]);
|
|
677
|
+
} catch (err) {
|
|
678
|
+
hookPromise.catch(() => {
|
|
679
|
+
});
|
|
680
|
+
throw err;
|
|
681
|
+
} finally {
|
|
682
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
683
|
+
}
|
|
585
684
|
}
|
|
586
685
|
async setup() {
|
|
587
686
|
const entriesExistedBeforeSetup = await this.db.getFirstAsync(
|
|
@@ -633,6 +732,9 @@ var _WikiMemory = class _WikiMemory {
|
|
|
633
732
|
);
|
|
634
733
|
}
|
|
635
734
|
}
|
|
735
|
+
if (entriesExistedBeforeSetup) {
|
|
736
|
+
await this.assertNoLegacySourceTypes();
|
|
737
|
+
}
|
|
636
738
|
const rows = await this.db.getAllAsync(`
|
|
637
739
|
SELECT rowid, source_ref FROM ${this.prefix}entries
|
|
638
740
|
WHERE source_ref IS NOT NULL
|
|
@@ -765,33 +867,77 @@ var _WikiMemory = class _WikiMemory {
|
|
|
765
867
|
let deletedEntries = 0;
|
|
766
868
|
let deletedTasks = 0;
|
|
767
869
|
let deletedEvents = 0;
|
|
768
|
-
const deletedEntryIds = [];
|
|
769
870
|
if (retainSoftDeletedFor !== null) {
|
|
770
871
|
const cutoff = now - retainSoftDeletedFor * 864e5;
|
|
771
872
|
const entriesToDelete = await this.db.getAllAsync(
|
|
772
|
-
`SELECT id FROM ${this.prefix}entries
|
|
773
|
-
WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at
|
|
873
|
+
`SELECT id, entity_id FROM ${this.prefix}entries
|
|
874
|
+
WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at <= ?`,
|
|
774
875
|
[entityId, cutoff]
|
|
775
876
|
);
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
877
|
+
const succeeded = [];
|
|
878
|
+
let failure = null;
|
|
879
|
+
for (const row of entriesToDelete) {
|
|
880
|
+
try {
|
|
881
|
+
await this._notifyEmbeddingPersistedOrThrow(row.entity_id, row.id, null);
|
|
882
|
+
succeeded.push({ entity_id: row.entity_id, id: row.id });
|
|
883
|
+
} catch (err) {
|
|
884
|
+
failure = { factId: row.id, cause: err };
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (succeeded.length > 0) {
|
|
889
|
+
const chunkSize = 500;
|
|
890
|
+
for (let i = 0; i < succeeded.length; i += chunkSize) {
|
|
891
|
+
const chunk = succeeded.slice(i, i + chunkSize);
|
|
892
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
893
|
+
const entryResult = await this.db.runAsync(
|
|
894
|
+
`DELETE FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at <= ? AND id IN (${placeholders})`,
|
|
895
|
+
[entityId, cutoff, ...chunk.map((r) => r.id)]
|
|
896
|
+
);
|
|
897
|
+
deletedEntries += entryResult.changes;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
783
900
|
const taskResult = await this.db.runAsync(
|
|
784
901
|
`DELETE FROM ${this.prefix}tasks
|
|
785
|
-
WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at
|
|
902
|
+
WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at <= ?`,
|
|
786
903
|
[entityId, cutoff]
|
|
787
904
|
);
|
|
788
905
|
deletedTasks = taskResult.changes;
|
|
906
|
+
if (failure) {
|
|
907
|
+
await this.rebuildMiniSearchIndex(entityId);
|
|
908
|
+
this.vectorCache.delete(entityId);
|
|
909
|
+
const remaining = entriesToDelete.length - succeeded.length - 1;
|
|
910
|
+
const isTimeout = failure.cause?.[HOOK_TIMEOUT_MARKER] === true;
|
|
911
|
+
if (isTimeout) {
|
|
912
|
+
throw new PrunePartialFailureError(
|
|
913
|
+
succeeded.length,
|
|
914
|
+
failure.factId,
|
|
915
|
+
remaining,
|
|
916
|
+
new Error("Deletion hook timed out"),
|
|
917
|
+
deletedTasks,
|
|
918
|
+
0
|
|
919
|
+
// events not yet deleted at this point
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
const errMsg = failure.cause?.message ?? "";
|
|
923
|
+
const isValidationError = errMsg.startsWith("Invalid deletionHookTimeoutMs");
|
|
924
|
+
const sanitizedCause = isValidationError ? failure.cause : this._sanitizeRankerError(failure.cause);
|
|
925
|
+
throw new PrunePartialFailureError(
|
|
926
|
+
succeeded.length,
|
|
927
|
+
failure.factId,
|
|
928
|
+
remaining,
|
|
929
|
+
sanitizedCause,
|
|
930
|
+
deletedTasks,
|
|
931
|
+
0
|
|
932
|
+
// events not yet deleted at this point
|
|
933
|
+
);
|
|
934
|
+
}
|
|
789
935
|
}
|
|
790
936
|
if (retainEventsFor !== null) {
|
|
791
937
|
const cutoff = now - retainEventsFor * 864e5;
|
|
792
938
|
const eventResult = await this.db.runAsync(
|
|
793
939
|
`DELETE FROM ${this.prefix}events
|
|
794
|
-
WHERE entity_id = ? AND created_at
|
|
940
|
+
WHERE entity_id = ? AND created_at <= ?`,
|
|
795
941
|
[entityId, cutoff]
|
|
796
942
|
);
|
|
797
943
|
deletedEvents = eventResult.changes;
|
|
@@ -802,14 +948,6 @@ var _WikiMemory = class _WikiMemory {
|
|
|
802
948
|
}
|
|
803
949
|
await this.rebuildMiniSearchIndex(entityId);
|
|
804
950
|
this.vectorCache.delete(entityId);
|
|
805
|
-
const uniqueDeletedIds = Array.from(new Set(deletedEntryIds));
|
|
806
|
-
for (const factId of uniqueDeletedIds) {
|
|
807
|
-
try {
|
|
808
|
-
await this._notifyEmbeddingPersisted(entityId, factId, null);
|
|
809
|
-
} catch (hookErr) {
|
|
810
|
-
console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during prune for ${factId}:`, hookErr);
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
951
|
return { entries: deletedEntries, tasks: deletedTasks, events: deletedEvents };
|
|
814
952
|
} finally {
|
|
815
953
|
this.activeMaintenanceJobs.delete(pruneKey);
|
|
@@ -1063,7 +1201,10 @@ var _WikiMemory = class _WikiMemory {
|
|
|
1063
1201
|
} catch (rankerErr) {
|
|
1064
1202
|
const rankerError = rankerErr instanceof Error ? rankerErr : new Error(String(rankerErr));
|
|
1065
1203
|
const policy = this.options.vectorRankerFallback ?? "js-cosine";
|
|
1066
|
-
this.options.onVectorRankerFallback?.({
|
|
1204
|
+
this.options.onVectorRankerFallback?.({
|
|
1205
|
+
error: this._sanitizeRankerError(rankerError),
|
|
1206
|
+
policy
|
|
1207
|
+
});
|
|
1067
1208
|
if (policy === "throw") {
|
|
1068
1209
|
rankerShouldRethrow = true;
|
|
1069
1210
|
throw rankerError;
|
|
@@ -1127,8 +1268,9 @@ var _WikiMemory = class _WikiMemory {
|
|
|
1127
1268
|
scored = [];
|
|
1128
1269
|
}
|
|
1129
1270
|
if (this.options.propagateRankerFailureToRetrievalFallback) {
|
|
1130
|
-
const mirrored = new Error("Vector ranker failed, falling back"
|
|
1131
|
-
|
|
1271
|
+
const mirrored = new Error("Vector ranker failed, falling back", {
|
|
1272
|
+
cause: this._sanitizeRankerError(rankerErr)
|
|
1273
|
+
});
|
|
1132
1274
|
pendingRankerFallbackError = mirrored;
|
|
1133
1275
|
}
|
|
1134
1276
|
}
|
|
@@ -1288,12 +1430,31 @@ var _WikiMemory = class _WikiMemory {
|
|
|
1288
1430
|
if (updatedAtDiff !== 0) return updatedAtDiff;
|
|
1289
1431
|
return a.id.localeCompare(b.id);
|
|
1290
1432
|
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Strip potentially sensitive data from ranker errors before exposing to host callbacks.
|
|
1435
|
+
* Preserves error type for debugging but removes message/stack that may contain credentials.
|
|
1436
|
+
* Recursively sanitizes one level of .cause; deeper chains collapse to type only.
|
|
1437
|
+
*/
|
|
1438
|
+
_sanitizeRankerError(err) {
|
|
1439
|
+
if (this.options.sanitizeRankerErrors === false) {
|
|
1440
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
1441
|
+
}
|
|
1442
|
+
const typeName = err instanceof Error ? err.constructor?.name ?? "Error" : typeof err;
|
|
1443
|
+
const innerCause = err instanceof Error && err.cause !== void 0 ? new Error(`Caused by: ${err.cause?.constructor?.name ?? typeof err.cause}`) : void 0;
|
|
1444
|
+
const sanitized = new Error(
|
|
1445
|
+
`VectorRanker ${typeName} (message scrubbed for security)`,
|
|
1446
|
+
innerCause ? { cause: innerCause } : void 0
|
|
1447
|
+
);
|
|
1448
|
+
sanitized.name = typeName;
|
|
1449
|
+
return sanitized;
|
|
1450
|
+
}
|
|
1291
1451
|
/**
|
|
1292
1452
|
* Score candidate rows using in-process JS cosine similarity.
|
|
1293
1453
|
* Applies hybrid blending (if weight set) and tie-break sorting before returning.
|
|
1294
1454
|
*/
|
|
1295
1455
|
async _rankWithJsCosine(args) {
|
|
1296
|
-
const
|
|
1456
|
+
const queryVec = args.queryVec instanceof Float32Array ? args.queryVec.slice() : Array.from(args.queryVec);
|
|
1457
|
+
const { entityId, candidateRows, weight, miniSearchScores, populateCache, limit } = args;
|
|
1297
1458
|
let entityCache = this.vectorCache.get(entityId);
|
|
1298
1459
|
const tooLarge = populateCache && candidateRows.length > _WikiMemory.MAX_VECTOR_CACHE_FACTS_PER_ENTITY;
|
|
1299
1460
|
if (tooLarge && entityCache) {
|
|
@@ -1344,14 +1505,15 @@ var _WikiMemory = class _WikiMemory {
|
|
|
1344
1505
|
* Returns scored results ready for hybrid blending and tie-break sorting.
|
|
1345
1506
|
*/
|
|
1346
1507
|
async _rankWithVectorRanker(args) {
|
|
1347
|
-
const { entityId,
|
|
1508
|
+
const { entityId, candidateIds, weight, miniSearchScores, limit } = args;
|
|
1348
1509
|
const ranker = this.options.vectorRanker;
|
|
1349
1510
|
if (!ranker) {
|
|
1350
1511
|
throw new Error("vectorRanker not configured");
|
|
1351
1512
|
}
|
|
1513
|
+
const queryVecCopy = args.queryVec instanceof Float32Array ? args.queryVec.slice() : Array.from(args.queryVec);
|
|
1352
1514
|
const rankerResults = await ranker.rankBySimilarity({
|
|
1353
1515
|
entityId,
|
|
1354
|
-
queryVec,
|
|
1516
|
+
queryVec: queryVecCopy,
|
|
1355
1517
|
candidateIds,
|
|
1356
1518
|
limit
|
|
1357
1519
|
});
|
|
@@ -1476,7 +1638,7 @@ ${JSON.stringify(currentFacts, null, 2)}`;
|
|
|
1476
1638
|
let skip = false;
|
|
1477
1639
|
if (newTokens.size >= MIN_TOKENS_TO_QUALIFY) {
|
|
1478
1640
|
for (const existing of currentFactsRows) {
|
|
1479
|
-
if (existing.source_type !== "
|
|
1641
|
+
if (existing.source_type !== "librarian_inferred") continue;
|
|
1480
1642
|
const existingTokens = titleTokens(existing.title);
|
|
1481
1643
|
if (existingTokens.size >= MIN_TOKENS_TO_QUALIFY) {
|
|
1482
1644
|
if (jaccardScore(newTokens, existingTokens) >= FUZZY_THRESHOLD) {
|
|
@@ -1491,7 +1653,7 @@ ${JSON.stringify(currentFacts, null, 2)}`;
|
|
|
1491
1653
|
await this.db.runAsync(`
|
|
1492
1654
|
INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, created_at, updated_at)
|
|
1493
1655
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1494
|
-
`, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "
|
|
1656
|
+
`, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "librarian_inferred", now, now]);
|
|
1495
1657
|
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
1496
1658
|
}
|
|
1497
1659
|
for (const task of validTasks) {
|
|
@@ -1524,25 +1686,25 @@ ${JSON.stringify(currentFacts, null, 2)}`;
|
|
|
1524
1686
|
if (orphanAfterDays !== null) {
|
|
1525
1687
|
const orphanThreshold = now - orphanAfterDays * MS_PER_DAY;
|
|
1526
1688
|
await this.db.runAsync(`
|
|
1527
|
-
UPDATE ${this.prefix}entries
|
|
1528
|
-
SET deleted_at = ?, updated_at = ?
|
|
1529
|
-
WHERE entity_id = ? AND access_count = 0 AND created_at
|
|
1689
|
+
UPDATE ${this.prefix}entries
|
|
1690
|
+
SET deleted_at = ?, updated_at = ?
|
|
1691
|
+
WHERE entity_id = ? AND access_count = 0 AND created_at <= ? AND source_type != 'immutable_document' AND deleted_at IS NULL
|
|
1530
1692
|
`, [now, now, entityId, orphanThreshold]);
|
|
1531
1693
|
}
|
|
1532
1694
|
if (staleInferredAfterDays !== null) {
|
|
1533
1695
|
const staleThreshold = now - staleInferredAfterDays * MS_PER_DAY;
|
|
1534
1696
|
await this.db.runAsync(`
|
|
1535
|
-
UPDATE ${this.prefix}entries
|
|
1536
|
-
SET confidence = 'tentative', updated_at = ?
|
|
1537
|
-
WHERE entity_id = ? AND confidence = 'inferred' AND (last_accessed_at
|
|
1697
|
+
UPDATE ${this.prefix}entries
|
|
1698
|
+
SET confidence = 'tentative', updated_at = ?
|
|
1699
|
+
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
|
|
1538
1700
|
`, [now, entityId, staleThreshold, staleThreshold]);
|
|
1539
1701
|
}
|
|
1540
1702
|
});
|
|
1541
1703
|
const allFactsRows = await this.db.getAllAsync(`SELECT * FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`, [entityId]);
|
|
1542
1704
|
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]);
|
|
1543
1705
|
const recentEvents = await this.db.getAllAsync(`SELECT * FROM ${this.prefix}events WHERE entity_id = ? ORDER BY created_at DESC LIMIT 20`, [entityId]);
|
|
1544
|
-
const healCandidates = allFactsRows.filter((f) => f.source_type !== "
|
|
1545
|
-
const documentAnchors = allFactsRows.filter((f) => f.source_type === "
|
|
1706
|
+
const healCandidates = allFactsRows.filter((f) => f.source_type !== "immutable_document");
|
|
1707
|
+
const documentAnchors = allFactsRows.filter((f) => f.source_type === "immutable_document").map(({ id, title, source_ref }) => ({ id, title, source_ref }));
|
|
1546
1708
|
const userPrompt = `Heal Candidates:
|
|
1547
1709
|
${JSON.stringify(healCandidates.map((f) => {
|
|
1548
1710
|
const { embedding: _embedding, embedding_blob: _blob, ...rest } = f;
|
|
@@ -1585,7 +1747,7 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1585
1747
|
await this.db.runAsync(`
|
|
1586
1748
|
INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, created_at, updated_at)
|
|
1587
1749
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1588
|
-
`, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "
|
|
1750
|
+
`, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "librarian_inferred", now, now]);
|
|
1589
1751
|
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
1590
1752
|
}
|
|
1591
1753
|
});
|
|
@@ -1885,6 +2047,7 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1885
2047
|
this.activeMaintenanceJobs.add(this._importKey(entityId));
|
|
1886
2048
|
}
|
|
1887
2049
|
try {
|
|
2050
|
+
await this.assertNoLegacySourceTypes();
|
|
1888
2051
|
for (const [entityId, bundle] of Object.entries(dump.entities)) {
|
|
1889
2052
|
await this._doImportEntity(entityId, bundle, merge);
|
|
1890
2053
|
}
|
|
@@ -1938,6 +2101,10 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1938
2101
|
}
|
|
1939
2102
|
}
|
|
1940
2103
|
for (const fact of bundle.facts) {
|
|
2104
|
+
const sourceType = this._normalizeImportedSourceType(String(fact.source_type), {
|
|
2105
|
+
entityId,
|
|
2106
|
+
factId: fact.id
|
|
2107
|
+
});
|
|
1941
2108
|
const tagsJson = JSON.stringify(Array.isArray(fact.tags) ? fact.tags : []);
|
|
1942
2109
|
const safeUpdatedAt = Number.isFinite(fact.updated_at) ? fact.updated_at : 0;
|
|
1943
2110
|
const existing = existingFactsById.get(fact.id);
|
|
@@ -1986,14 +2153,14 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1986
2153
|
if (blobData != null) {
|
|
1987
2154
|
await this.db.runAsync(
|
|
1988
2155
|
`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 = ?`,
|
|
1989
|
-
[entityId, fact.title, fact.body, tagsJson, fact.confidence,
|
|
2156
|
+
[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]
|
|
1990
2157
|
);
|
|
1991
2158
|
factsWithPreservedBlob.set(fact.id, blobData);
|
|
1992
2159
|
if (!fact.deleted_at) preservedBlobDims.add(blobData.byteLength / 4);
|
|
1993
2160
|
} else {
|
|
1994
2161
|
await this.db.runAsync(
|
|
1995
2162
|
`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 = ?`,
|
|
1996
|
-
[entityId, fact.title, fact.body, tagsJson, fact.confidence,
|
|
2163
|
+
[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]
|
|
1997
2164
|
);
|
|
1998
2165
|
}
|
|
1999
2166
|
existingFactsById.set(fact.id, { id: fact.id, entity_id: entityId, updated_at: safeUpdatedAt });
|
|
@@ -2003,14 +2170,14 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
2003
2170
|
if (blobData != null) {
|
|
2004
2171
|
await this.db.runAsync(
|
|
2005
2172
|
`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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2006
|
-
[fact.id, entityId, fact.title, fact.body, tagsJson, fact.confidence,
|
|
2173
|
+
[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]
|
|
2007
2174
|
);
|
|
2008
2175
|
factsWithPreservedBlob.set(fact.id, blobData);
|
|
2009
2176
|
if (!fact.deleted_at) preservedBlobDims.add(blobData.byteLength / 4);
|
|
2010
2177
|
} else {
|
|
2011
2178
|
await this.db.runAsync(
|
|
2012
2179
|
`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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2013
|
-
[fact.id, entityId, fact.title, fact.body, tagsJson, fact.confidence,
|
|
2180
|
+
[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]
|
|
2014
2181
|
);
|
|
2015
2182
|
}
|
|
2016
2183
|
existingFactsById.set(fact.id, { id: fact.id, entity_id: entityId, updated_at: safeUpdatedAt });
|
|
@@ -2171,11 +2338,15 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
2171
2338
|
let deletedTasks = 0;
|
|
2172
2339
|
const deletedEntryIds = [];
|
|
2173
2340
|
if (params.clearAll) {
|
|
2174
|
-
const
|
|
2341
|
+
const newDeletions = await this.db.getAllAsync(
|
|
2175
2342
|
`SELECT id FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`,
|
|
2176
2343
|
[entityId]
|
|
2177
2344
|
);
|
|
2178
|
-
|
|
2345
|
+
const alreadySoftDeleted = await this.db.getAllAsync(
|
|
2346
|
+
`SELECT id FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NOT NULL`,
|
|
2347
|
+
[entityId]
|
|
2348
|
+
);
|
|
2349
|
+
deletedEntryIds.push(...newDeletions.map((e) => e.id), ...alreadySoftDeleted.map((e) => e.id));
|
|
2179
2350
|
const [entriesRes, tasksRes] = await Promise.all([
|
|
2180
2351
|
this.db.runAsync(`UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE entity_id = ? AND deleted_at IS NULL`, [now, now, entityId]),
|
|
2181
2352
|
this.db.runAsync(`UPDATE ${this.prefix}tasks SET deleted_at = ?, updated_at = ? WHERE entity_id = ? AND deleted_at IS NULL`, [now, now, entityId])
|
|
@@ -2195,13 +2366,13 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
2195
2366
|
if (params.sourceHash !== void 0 && !sourceHash) throw new Error("Invalid sourceHash (must be 64-char hex string)");
|
|
2196
2367
|
if (params.entryId) {
|
|
2197
2368
|
const entry = await this.db.getFirstAsync(
|
|
2198
|
-
`SELECT id FROM ${this.prefix}entries WHERE id = ? AND entity_id =
|
|
2369
|
+
`SELECT id FROM ${this.prefix}entries WHERE id = ? AND entity_id = ?`,
|
|
2199
2370
|
[params.entryId, entityId]
|
|
2200
2371
|
);
|
|
2201
2372
|
if (entry) deletedEntryIds.push(entry.id);
|
|
2202
2373
|
}
|
|
2203
2374
|
if (sourceRef || sourceHash) {
|
|
2204
|
-
let q = `SELECT id FROM ${this.prefix}entries WHERE entity_id =
|
|
2375
|
+
let q = `SELECT id FROM ${this.prefix}entries WHERE entity_id = ?`;
|
|
2205
2376
|
const args = [entityId];
|
|
2206
2377
|
if (sourceRef) {
|
|
2207
2378
|
q += ` AND source_ref = ?`;
|
|
@@ -2244,9 +2415,26 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
2244
2415
|
const uniqueDeletedIds = Array.from(new Set(deletedEntryIds));
|
|
2245
2416
|
for (const factId of uniqueDeletedIds) {
|
|
2246
2417
|
try {
|
|
2247
|
-
await this.
|
|
2418
|
+
await this._notifyEmbeddingPersistedOrThrow(entityId, factId, null);
|
|
2248
2419
|
} catch (hookErr) {
|
|
2249
|
-
|
|
2420
|
+
const isTimeout = hookErr?.[HOOK_TIMEOUT_MARKER] === true;
|
|
2421
|
+
if (isTimeout) {
|
|
2422
|
+
throw new Error(
|
|
2423
|
+
`forget(${entityId}/${factId}) failed: ${hookErr.message}`
|
|
2424
|
+
);
|
|
2425
|
+
}
|
|
2426
|
+
const errMsg = hookErr?.message ?? "";
|
|
2427
|
+
const isValidationError = errMsg.startsWith("Invalid deletionHookTimeoutMs");
|
|
2428
|
+
if (isValidationError) {
|
|
2429
|
+
throw new Error(
|
|
2430
|
+
`forget(${entityId}/${factId}) failed: ${errMsg}`,
|
|
2431
|
+
{ cause: hookErr }
|
|
2432
|
+
);
|
|
2433
|
+
}
|
|
2434
|
+
throw new Error(
|
|
2435
|
+
`forget(${entityId}/${factId}) failed: ANN cleanup hook rejected`,
|
|
2436
|
+
{ cause: this._sanitizeRankerError(hookErr) }
|
|
2437
|
+
);
|
|
2250
2438
|
}
|
|
2251
2439
|
}
|
|
2252
2440
|
return { deleted: { entries: deletedEntries, tasks: deletedTasks } };
|
|
@@ -2336,7 +2524,7 @@ ${chunk}`;
|
|
|
2336
2524
|
await this.db.runAsync(
|
|
2337
2525
|
`INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, source_hash, source_ref, created_at, updated_at)
|
|
2338
2526
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2339
|
-
[id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "
|
|
2527
|
+
[id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "immutable_document", sourceHash, sourceRef, now, now]
|
|
2340
2528
|
);
|
|
2341
2529
|
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
2342
2530
|
}
|
|
@@ -2595,10 +2783,12 @@ function createWiki(db, options) {
|
|
|
2595
2783
|
return new WikiMemory(db, options);
|
|
2596
2784
|
}
|
|
2597
2785
|
|
|
2786
|
+
exports.PrunePartialFailureError = PrunePartialFailureError;
|
|
2598
2787
|
exports.WikiBusyError = WikiBusyError;
|
|
2599
2788
|
exports.WikiMemory = WikiMemory;
|
|
2600
2789
|
exports.createWiki = createWiki;
|
|
2601
2790
|
exports.formatContext = formatContext;
|
|
2602
2791
|
exports.formatMemoryDump = formatMemoryDump;
|
|
2792
|
+
exports.parseEmbedding = parseEmbedding;
|
|
2603
2793
|
//# sourceMappingURL=index.js.map
|
|
2604
2794
|
//# sourceMappingURL=index.js.map
|