@equationalapplications/core-llm-wiki 3.2.0 → 4.1.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 +35 -0
- package/dist/index.d.mts +29 -2
- package/dist/index.d.ts +29 -2
- package/dist/index.js +134 -22
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +134 -23
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -302,6 +302,41 @@ wikiMemory.clearVectorCache();
|
|
|
302
302
|
|
|
303
303
|
The cache is also automatically invalidated on any mutation (`runLibrarian`, `runHeal`, `runPrune`, `runReembed`, `ingestDocument`, `importDump`, `forget`).
|
|
304
304
|
|
|
305
|
+
## Entity Status
|
|
306
|
+
|
|
307
|
+
`WikiMemory` exposes the in-flight job state for a single entity through two complementary APIs.
|
|
308
|
+
|
|
309
|
+
### `getEntityStatus(entityId)`
|
|
310
|
+
|
|
311
|
+
Synchronous point-in-time snapshot:
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
const status = wikiMemory.getEntityStatus('user-42');
|
|
315
|
+
// { ingesting: boolean, librarian: boolean, heal: boolean }
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Use this when you only need the current value (e.g. inside a request handler).
|
|
319
|
+
|
|
320
|
+
### `subscribeEntityStatus(entityId, callback)`
|
|
321
|
+
|
|
322
|
+
Push-based change notification — the callback fires synchronously once with the current status, then again on every transition where any of the three booleans flips. There is no polling and no duplicate snapshots.
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
const unsubscribe = wikiMemory.subscribeEntityStatus('user-42', (status) => {
|
|
326
|
+
console.log(status); // { ingesting, librarian, heal }
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Later:
|
|
330
|
+
unsubscribe(); // idempotent — safe to call more than once
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Notes:
|
|
334
|
+
|
|
335
|
+
- The first invocation happens **before** `subscribeEntityStatus` returns. Treat it as the initial render value.
|
|
336
|
+
- Each emission may be a fresh object literal. Do not rely on referential equality between callbacks; equality of the three booleans is the contract.
|
|
337
|
+
- A throwing callback is caught (logged via `console.error`) and does not block other subscribers or the underlying job.
|
|
338
|
+
- Subscriptions are scoped to a single `entityId`. There is no wildcard or "all entities" form.
|
|
339
|
+
|
|
305
340
|
## Security
|
|
306
341
|
|
|
307
342
|
`@equationalapplications/core-llm-wiki` enforces multiple security layers:
|
package/dist/index.d.mts
CHANGED
|
@@ -60,7 +60,15 @@ interface WikiFact {
|
|
|
60
60
|
body: string;
|
|
61
61
|
tags: string[];
|
|
62
62
|
confidence: 'certain' | 'inferred' | 'tentative';
|
|
63
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Source type of this fact.
|
|
65
|
+
* - 'immutable_document': From ingestDocument(), cannot be modified by system (librarian/heal).
|
|
66
|
+
* Only removable via forget() or replaced via re-ingest.
|
|
67
|
+
* - 'librarian_inferred': Created by runLibrarian() from events, or by runHeal() when synthesizing new inferred facts.
|
|
68
|
+
* - 'user_stated': Direct user statement.
|
|
69
|
+
* - 'user_confirmed': User-confirmed fact.
|
|
70
|
+
*/
|
|
71
|
+
source_type: 'user_stated' | 'librarian_inferred' | 'user_confirmed' | 'immutable_document';
|
|
64
72
|
source_hash: string | null;
|
|
65
73
|
source_ref: string | null;
|
|
66
74
|
created_at: number;
|
|
@@ -329,6 +337,7 @@ declare class WikiMemory {
|
|
|
329
337
|
private options;
|
|
330
338
|
private activeMaintenanceJobs;
|
|
331
339
|
private activeIngestJobs;
|
|
340
|
+
private statusSubscribers;
|
|
332
341
|
private miniSearch;
|
|
333
342
|
private miniSearchEntryIdsByEntity;
|
|
334
343
|
/**
|
|
@@ -358,6 +367,9 @@ declare class WikiMemory {
|
|
|
358
367
|
private _librarianKey;
|
|
359
368
|
private _healKey;
|
|
360
369
|
private _warnCrossEntityCollision;
|
|
370
|
+
/** Maps pre-rename enum strings from older dumps to current source_type values. */
|
|
371
|
+
private _normalizeImportedSourceType;
|
|
372
|
+
private assertNoLegacySourceTypes;
|
|
361
373
|
private _notifyEmbeddingPersisted;
|
|
362
374
|
/**
|
|
363
375
|
* GDPR-critical variant: awaits the hook with a timeout and rethrows failures.
|
|
@@ -381,6 +393,8 @@ declare class WikiMemory {
|
|
|
381
393
|
private _isAnyMaintenanceActiveWithSuffix;
|
|
382
394
|
/** Returns true if any ingest job is active for the given entity. */
|
|
383
395
|
private _isIngestActiveFor;
|
|
396
|
+
private _copyEntityStatus;
|
|
397
|
+
private _notifyStatusSubscribers;
|
|
384
398
|
private _validatePruneDuration;
|
|
385
399
|
runPrune(entityId: string, options?: {
|
|
386
400
|
retainSoftDeletedFor?: number | null;
|
|
@@ -434,6 +448,17 @@ declare class WikiMemory {
|
|
|
434
448
|
failed: number;
|
|
435
449
|
}>;
|
|
436
450
|
getEntityStatus(entityId: string): EntityStatus;
|
|
451
|
+
/**
|
|
452
|
+
* Subscribe to {@link EntityStatus} changes for a single entity. The callback
|
|
453
|
+
* is invoked synchronously once with the current status before this method
|
|
454
|
+
* returns, then again on every transition where any of `ingesting`,
|
|
455
|
+
* `librarian`, or `heal` flips. No polling, no duplicate snapshots.
|
|
456
|
+
*
|
|
457
|
+
* Returns an idempotent unsubscribe function.
|
|
458
|
+
*
|
|
459
|
+
* See also {@link getEntityStatus} for a synchronous point-in-time read.
|
|
460
|
+
*/
|
|
461
|
+
subscribeEntityStatus(entityId: string, callback: (status: EntityStatus) => void): () => void;
|
|
437
462
|
clearVectorCache(): void;
|
|
438
463
|
private _getFullBundle;
|
|
439
464
|
exportDump(entityIds?: string[]): Promise<MemoryDump>;
|
|
@@ -470,6 +495,8 @@ declare function formatContext(bundle: MemoryBundle, options?: FormatContextOpti
|
|
|
470
495
|
|
|
471
496
|
declare function formatMemoryDump(dump: MemoryDump): FormattedMemoryDump;
|
|
472
497
|
|
|
498
|
+
declare function parseEmbedding(blob: Uint8Array | null | undefined, text: string | null | undefined): Float32Array | null;
|
|
499
|
+
|
|
473
500
|
declare function createWiki(db: SQLiteAdapter, options: WikiOptions): WikiMemory;
|
|
474
501
|
|
|
475
|
-
export { type EntityStatus, type ExtractedFact, type ExtractedTask, type FormatContextOptions, type FormattedMemoryDump, type LLMProvider, type MemoryBundle, type MemoryDump, PrunePartialFailureError, type ReadOptions, type SQLiteAdapter, type VectorRanker, type VectorRankerFallback, type VectorRankerRankArgs, type VectorRankerSemanticResult, WikiBusyError, type WikiBusyOperation, type WikiCheckpoint, type WikiConfig, type WikiEvent, type WikiFact, WikiMemory, type WikiOptions, type WikiTask, createWiki, formatContext, formatMemoryDump };
|
|
502
|
+
export { type EntityStatus, type ExtractedFact, type ExtractedTask, type FormatContextOptions, type FormattedMemoryDump, type LLMProvider, type MemoryBundle, type MemoryDump, PrunePartialFailureError, type ReadOptions, type SQLiteAdapter, type VectorRanker, type VectorRankerFallback, type VectorRankerRankArgs, type VectorRankerSemanticResult, WikiBusyError, type WikiBusyOperation, type WikiCheckpoint, type WikiConfig, type WikiEvent, type WikiFact, WikiMemory, type WikiOptions, type WikiTask, createWiki, formatContext, formatMemoryDump, parseEmbedding };
|
package/dist/index.d.ts
CHANGED
|
@@ -60,7 +60,15 @@ interface WikiFact {
|
|
|
60
60
|
body: string;
|
|
61
61
|
tags: string[];
|
|
62
62
|
confidence: 'certain' | 'inferred' | 'tentative';
|
|
63
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Source type of this fact.
|
|
65
|
+
* - 'immutable_document': From ingestDocument(), cannot be modified by system (librarian/heal).
|
|
66
|
+
* Only removable via forget() or replaced via re-ingest.
|
|
67
|
+
* - 'librarian_inferred': Created by runLibrarian() from events, or by runHeal() when synthesizing new inferred facts.
|
|
68
|
+
* - 'user_stated': Direct user statement.
|
|
69
|
+
* - 'user_confirmed': User-confirmed fact.
|
|
70
|
+
*/
|
|
71
|
+
source_type: 'user_stated' | 'librarian_inferred' | 'user_confirmed' | 'immutable_document';
|
|
64
72
|
source_hash: string | null;
|
|
65
73
|
source_ref: string | null;
|
|
66
74
|
created_at: number;
|
|
@@ -329,6 +337,7 @@ declare class WikiMemory {
|
|
|
329
337
|
private options;
|
|
330
338
|
private activeMaintenanceJobs;
|
|
331
339
|
private activeIngestJobs;
|
|
340
|
+
private statusSubscribers;
|
|
332
341
|
private miniSearch;
|
|
333
342
|
private miniSearchEntryIdsByEntity;
|
|
334
343
|
/**
|
|
@@ -358,6 +367,9 @@ declare class WikiMemory {
|
|
|
358
367
|
private _librarianKey;
|
|
359
368
|
private _healKey;
|
|
360
369
|
private _warnCrossEntityCollision;
|
|
370
|
+
/** Maps pre-rename enum strings from older dumps to current source_type values. */
|
|
371
|
+
private _normalizeImportedSourceType;
|
|
372
|
+
private assertNoLegacySourceTypes;
|
|
361
373
|
private _notifyEmbeddingPersisted;
|
|
362
374
|
/**
|
|
363
375
|
* GDPR-critical variant: awaits the hook with a timeout and rethrows failures.
|
|
@@ -381,6 +393,8 @@ declare class WikiMemory {
|
|
|
381
393
|
private _isAnyMaintenanceActiveWithSuffix;
|
|
382
394
|
/** Returns true if any ingest job is active for the given entity. */
|
|
383
395
|
private _isIngestActiveFor;
|
|
396
|
+
private _copyEntityStatus;
|
|
397
|
+
private _notifyStatusSubscribers;
|
|
384
398
|
private _validatePruneDuration;
|
|
385
399
|
runPrune(entityId: string, options?: {
|
|
386
400
|
retainSoftDeletedFor?: number | null;
|
|
@@ -434,6 +448,17 @@ declare class WikiMemory {
|
|
|
434
448
|
failed: number;
|
|
435
449
|
}>;
|
|
436
450
|
getEntityStatus(entityId: string): EntityStatus;
|
|
451
|
+
/**
|
|
452
|
+
* Subscribe to {@link EntityStatus} changes for a single entity. The callback
|
|
453
|
+
* is invoked synchronously once with the current status before this method
|
|
454
|
+
* returns, then again on every transition where any of `ingesting`,
|
|
455
|
+
* `librarian`, or `heal` flips. No polling, no duplicate snapshots.
|
|
456
|
+
*
|
|
457
|
+
* Returns an idempotent unsubscribe function.
|
|
458
|
+
*
|
|
459
|
+
* See also {@link getEntityStatus} for a synchronous point-in-time read.
|
|
460
|
+
*/
|
|
461
|
+
subscribeEntityStatus(entityId: string, callback: (status: EntityStatus) => void): () => void;
|
|
437
462
|
clearVectorCache(): void;
|
|
438
463
|
private _getFullBundle;
|
|
439
464
|
exportDump(entityIds?: string[]): Promise<MemoryDump>;
|
|
@@ -470,6 +495,8 @@ declare function formatContext(bundle: MemoryBundle, options?: FormatContextOpti
|
|
|
470
495
|
|
|
471
496
|
declare function formatMemoryDump(dump: MemoryDump): FormattedMemoryDump;
|
|
472
497
|
|
|
498
|
+
declare function parseEmbedding(blob: Uint8Array | null | undefined, text: string | null | undefined): Float32Array | null;
|
|
499
|
+
|
|
473
500
|
declare function createWiki(db: SQLiteAdapter, options: WikiOptions): WikiMemory;
|
|
474
501
|
|
|
475
|
-
export { type EntityStatus, type ExtractedFact, type ExtractedTask, type FormatContextOptions, type FormattedMemoryDump, type LLMProvider, type MemoryBundle, type MemoryDump, PrunePartialFailureError, type ReadOptions, type SQLiteAdapter, type VectorRanker, type VectorRankerFallback, type VectorRankerRankArgs, type VectorRankerSemanticResult, WikiBusyError, type WikiBusyOperation, type WikiCheckpoint, type WikiConfig, type WikiEvent, type WikiFact, WikiMemory, type WikiOptions, type WikiTask, createWiki, formatContext, formatMemoryDump };
|
|
502
|
+
export { type EntityStatus, type ExtractedFact, type ExtractedTask, type FormatContextOptions, type FormattedMemoryDump, type LLMProvider, type MemoryBundle, type MemoryDump, PrunePartialFailureError, type ReadOptions, type SQLiteAdapter, type VectorRanker, type VectorRankerFallback, type VectorRankerRankArgs, type VectorRankerSemanticResult, WikiBusyError, type WikiBusyOperation, type WikiCheckpoint, type WikiConfig, type WikiEvent, type WikiFact, WikiMemory, type WikiOptions, type WikiTask, createWiki, formatContext, formatMemoryDump, parseEmbedding };
|
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,
|
|
@@ -418,6 +418,7 @@ var _WikiMemory = class _WikiMemory {
|
|
|
418
418
|
constructor(db, options) {
|
|
419
419
|
this.activeMaintenanceJobs = /* @__PURE__ */ new Set();
|
|
420
420
|
this.activeIngestJobs = /* @__PURE__ */ new Set();
|
|
421
|
+
this.statusSubscribers = /* @__PURE__ */ new Map();
|
|
421
422
|
this.miniSearch = new MiniSearch__default.default({
|
|
422
423
|
fields: ["title", "body", "tags"],
|
|
423
424
|
storeFields: ["entity_id"],
|
|
@@ -593,6 +594,44 @@ var _WikiMemory = class _WikiMemory {
|
|
|
593
594
|
_warnCrossEntityCollision(type, id, existingEntityId, targetEntityId) {
|
|
594
595
|
console.warn(`[WikiMemory] importDump: ${type} id "${id}" already belongs to entity "${existingEntityId}"; skipping for entity "${targetEntityId}"`);
|
|
595
596
|
}
|
|
597
|
+
/** Maps pre-rename enum strings from older dumps to current source_type values. */
|
|
598
|
+
_normalizeImportedSourceType(raw, ctx) {
|
|
599
|
+
if (raw === "user_document") return "immutable_document";
|
|
600
|
+
if (raw === "agent_inferred") return "librarian_inferred";
|
|
601
|
+
const allowed = ["user_stated", "librarian_inferred", "user_confirmed", "immutable_document"];
|
|
602
|
+
if (allowed.includes(raw)) return raw;
|
|
603
|
+
const where = ctx !== void 0 ? ` for entity "${ctx.entityId}" fact "${ctx.factId}"` : "";
|
|
604
|
+
throw new Error(
|
|
605
|
+
`importDump: invalid source_type "${raw}"${where} (expected one of: ${allowed.join(", ")}, or legacy aliases user_document / agent_inferred)`
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
async assertNoLegacySourceTypes() {
|
|
609
|
+
const legacyProbe = await this.db.getFirstAsync(
|
|
610
|
+
`SELECT 1 AS one FROM ${this.prefix}entries
|
|
611
|
+
WHERE source_type IN ('user_document', 'agent_inferred')
|
|
612
|
+
LIMIT 1`,
|
|
613
|
+
[]
|
|
614
|
+
);
|
|
615
|
+
if (!legacyProbe) return;
|
|
616
|
+
const legacyCount = await this.db.getFirstAsync(
|
|
617
|
+
`SELECT COUNT(*) as count FROM ${this.prefix}entries
|
|
618
|
+
WHERE source_type IN ('user_document', 'agent_inferred')`,
|
|
619
|
+
[]
|
|
620
|
+
);
|
|
621
|
+
const count = legacyCount?.count ?? 0;
|
|
622
|
+
const migrationSQL = `
|
|
623
|
+
-- Migrate legacy source_type values (targets your WikiMemory prefix: ${this.prefix})
|
|
624
|
+
UPDATE ${this.prefix}entries SET source_type = 'immutable_document' WHERE source_type = 'user_document';
|
|
625
|
+
UPDATE ${this.prefix}entries SET source_type = 'librarian_inferred' WHERE source_type = 'agent_inferred';
|
|
626
|
+
`.trim();
|
|
627
|
+
throw new Error(
|
|
628
|
+
`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.
|
|
629
|
+
|
|
630
|
+
${migrationSQL}
|
|
631
|
+
|
|
632
|
+
After running the migration SQL, restart your application.`
|
|
633
|
+
);
|
|
634
|
+
}
|
|
596
635
|
async _notifyEmbeddingPersisted(entityId, factId, vector) {
|
|
597
636
|
if (!this.options.vectorRanker?.onEmbeddingPersisted) return;
|
|
598
637
|
const vectorCopy = vector ? vector.slice() : null;
|
|
@@ -694,6 +733,9 @@ var _WikiMemory = class _WikiMemory {
|
|
|
694
733
|
);
|
|
695
734
|
}
|
|
696
735
|
}
|
|
736
|
+
if (entriesExistedBeforeSetup) {
|
|
737
|
+
await this.assertNoLegacySourceTypes();
|
|
738
|
+
}
|
|
697
739
|
const rows = await this.db.getAllAsync(`
|
|
698
740
|
SELECT rowid, source_ref FROM ${this.prefix}entries
|
|
699
741
|
WHERE source_ref IS NOT NULL
|
|
@@ -781,6 +823,24 @@ var _WikiMemory = class _WikiMemory {
|
|
|
781
823
|
}
|
|
782
824
|
return false;
|
|
783
825
|
}
|
|
826
|
+
_copyEntityStatus(s) {
|
|
827
|
+
return { ingesting: s.ingesting, librarian: s.librarian, heal: s.heal };
|
|
828
|
+
}
|
|
829
|
+
_notifyStatusSubscribers(entityId) {
|
|
830
|
+
const set = this.statusSubscribers.get(entityId);
|
|
831
|
+
if (!set || set.size === 0) return;
|
|
832
|
+
for (const entry of Array.from(set)) {
|
|
833
|
+
if (!set.has(entry)) continue;
|
|
834
|
+
const next = this.getEntityStatus(entityId);
|
|
835
|
+
if (entry.last.ingesting === next.ingesting && entry.last.librarian === next.librarian && entry.last.heal === next.heal) continue;
|
|
836
|
+
entry.last = this._copyEntityStatus(next);
|
|
837
|
+
try {
|
|
838
|
+
entry.callback(this._copyEntityStatus(next));
|
|
839
|
+
} catch (err) {
|
|
840
|
+
console.error(`[WikiMemory.subscribeEntityStatus] callback error for entityId="${entityId}" during transition emission`, err);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
784
844
|
_validatePruneDuration(value, name) {
|
|
785
845
|
if (value !== null && value !== void 0 && (typeof value !== "number" || !isFinite(value) || value < 0)) {
|
|
786
846
|
throw new Error(`Invalid ${name}: must be a non-negative finite number or null`);
|
|
@@ -830,7 +890,7 @@ var _WikiMemory = class _WikiMemory {
|
|
|
830
890
|
const cutoff = now - retainSoftDeletedFor * 864e5;
|
|
831
891
|
const entriesToDelete = await this.db.getAllAsync(
|
|
832
892
|
`SELECT id, entity_id FROM ${this.prefix}entries
|
|
833
|
-
WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at
|
|
893
|
+
WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at <= ?`,
|
|
834
894
|
[entityId, cutoff]
|
|
835
895
|
);
|
|
836
896
|
const succeeded = [];
|
|
@@ -850,7 +910,7 @@ var _WikiMemory = class _WikiMemory {
|
|
|
850
910
|
const chunk = succeeded.slice(i, i + chunkSize);
|
|
851
911
|
const placeholders = chunk.map(() => "?").join(",");
|
|
852
912
|
const entryResult = await this.db.runAsync(
|
|
853
|
-
`DELETE FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at
|
|
913
|
+
`DELETE FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at <= ? AND id IN (${placeholders})`,
|
|
854
914
|
[entityId, cutoff, ...chunk.map((r) => r.id)]
|
|
855
915
|
);
|
|
856
916
|
deletedEntries += entryResult.changes;
|
|
@@ -858,7 +918,7 @@ var _WikiMemory = class _WikiMemory {
|
|
|
858
918
|
}
|
|
859
919
|
const taskResult = await this.db.runAsync(
|
|
860
920
|
`DELETE FROM ${this.prefix}tasks
|
|
861
|
-
WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at
|
|
921
|
+
WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at <= ?`,
|
|
862
922
|
[entityId, cutoff]
|
|
863
923
|
);
|
|
864
924
|
deletedTasks = taskResult.changes;
|
|
@@ -896,7 +956,7 @@ var _WikiMemory = class _WikiMemory {
|
|
|
896
956
|
const cutoff = now - retainEventsFor * 864e5;
|
|
897
957
|
const eventResult = await this.db.runAsync(
|
|
898
958
|
`DELETE FROM ${this.prefix}events
|
|
899
|
-
WHERE entity_id = ? AND created_at
|
|
959
|
+
WHERE entity_id = ? AND created_at <= ?`,
|
|
900
960
|
[entityId, cutoff]
|
|
901
961
|
);
|
|
902
962
|
deletedEvents = eventResult.changes;
|
|
@@ -1523,7 +1583,11 @@ var _WikiMemory = class _WikiMemory {
|
|
|
1523
1583
|
const jobKey = this._librarianKey(entityId);
|
|
1524
1584
|
if (!this.activeMaintenanceJobs.has(jobKey) && !this.activeMaintenanceJobs.has(this._pruneKey(entityId)) && !this._isReembedActive(entityId) && !this._isImportActiveFor(entityId) && !this._isForgetActiveFor(entityId)) {
|
|
1525
1585
|
this.activeMaintenanceJobs.add(jobKey);
|
|
1526
|
-
this.
|
|
1586
|
+
this._notifyStatusSubscribers(entityId);
|
|
1587
|
+
this.runLibrarianThenMaybeHeal(entityId, count).catch(console.error).finally(() => {
|
|
1588
|
+
this.activeMaintenanceJobs.delete(jobKey);
|
|
1589
|
+
this._notifyStatusSubscribers(entityId);
|
|
1590
|
+
});
|
|
1527
1591
|
}
|
|
1528
1592
|
}
|
|
1529
1593
|
}
|
|
@@ -1542,6 +1606,7 @@ var _WikiMemory = class _WikiMemory {
|
|
|
1542
1606
|
const healKey = this._healKey(entityId);
|
|
1543
1607
|
if (!this.activeMaintenanceJobs.has(healKey)) {
|
|
1544
1608
|
this.activeMaintenanceJobs.add(healKey);
|
|
1609
|
+
this._notifyStatusSubscribers(entityId);
|
|
1545
1610
|
try {
|
|
1546
1611
|
await this._doRunHeal(entityId);
|
|
1547
1612
|
await this.db.runAsync(`
|
|
@@ -1551,6 +1616,7 @@ var _WikiMemory = class _WikiMemory {
|
|
|
1551
1616
|
`, [entityId, currentEventCount, currentEventCount]);
|
|
1552
1617
|
} finally {
|
|
1553
1618
|
this.activeMaintenanceJobs.delete(healKey);
|
|
1619
|
+
this._notifyStatusSubscribers(entityId);
|
|
1554
1620
|
}
|
|
1555
1621
|
}
|
|
1556
1622
|
}
|
|
@@ -1597,7 +1663,7 @@ ${JSON.stringify(currentFacts, null, 2)}`;
|
|
|
1597
1663
|
let skip = false;
|
|
1598
1664
|
if (newTokens.size >= MIN_TOKENS_TO_QUALIFY) {
|
|
1599
1665
|
for (const existing of currentFactsRows) {
|
|
1600
|
-
if (existing.source_type !== "
|
|
1666
|
+
if (existing.source_type !== "librarian_inferred") continue;
|
|
1601
1667
|
const existingTokens = titleTokens(existing.title);
|
|
1602
1668
|
if (existingTokens.size >= MIN_TOKENS_TO_QUALIFY) {
|
|
1603
1669
|
if (jaccardScore(newTokens, existingTokens) >= FUZZY_THRESHOLD) {
|
|
@@ -1612,7 +1678,7 @@ ${JSON.stringify(currentFacts, null, 2)}`;
|
|
|
1612
1678
|
await this.db.runAsync(`
|
|
1613
1679
|
INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, created_at, updated_at)
|
|
1614
1680
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1615
|
-
`, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "
|
|
1681
|
+
`, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "librarian_inferred", now, now]);
|
|
1616
1682
|
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
1617
1683
|
}
|
|
1618
1684
|
for (const task of validTasks) {
|
|
@@ -1645,25 +1711,25 @@ ${JSON.stringify(currentFacts, null, 2)}`;
|
|
|
1645
1711
|
if (orphanAfterDays !== null) {
|
|
1646
1712
|
const orphanThreshold = now - orphanAfterDays * MS_PER_DAY;
|
|
1647
1713
|
await this.db.runAsync(`
|
|
1648
|
-
UPDATE ${this.prefix}entries
|
|
1649
|
-
SET deleted_at = ?, updated_at = ?
|
|
1650
|
-
WHERE entity_id = ? AND access_count = 0 AND created_at
|
|
1714
|
+
UPDATE ${this.prefix}entries
|
|
1715
|
+
SET deleted_at = ?, updated_at = ?
|
|
1716
|
+
WHERE entity_id = ? AND access_count = 0 AND created_at <= ? AND source_type != 'immutable_document' AND deleted_at IS NULL
|
|
1651
1717
|
`, [now, now, entityId, orphanThreshold]);
|
|
1652
1718
|
}
|
|
1653
1719
|
if (staleInferredAfterDays !== null) {
|
|
1654
1720
|
const staleThreshold = now - staleInferredAfterDays * MS_PER_DAY;
|
|
1655
1721
|
await this.db.runAsync(`
|
|
1656
|
-
UPDATE ${this.prefix}entries
|
|
1657
|
-
SET confidence = 'tentative', updated_at = ?
|
|
1658
|
-
WHERE entity_id = ? AND confidence = 'inferred' AND (last_accessed_at
|
|
1722
|
+
UPDATE ${this.prefix}entries
|
|
1723
|
+
SET confidence = 'tentative', updated_at = ?
|
|
1724
|
+
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
|
|
1659
1725
|
`, [now, entityId, staleThreshold, staleThreshold]);
|
|
1660
1726
|
}
|
|
1661
1727
|
});
|
|
1662
1728
|
const allFactsRows = await this.db.getAllAsync(`SELECT * FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`, [entityId]);
|
|
1663
1729
|
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]);
|
|
1664
1730
|
const recentEvents = await this.db.getAllAsync(`SELECT * FROM ${this.prefix}events WHERE entity_id = ? ORDER BY created_at DESC LIMIT 20`, [entityId]);
|
|
1665
|
-
const healCandidates = allFactsRows.filter((f) => f.source_type !== "
|
|
1666
|
-
const documentAnchors = allFactsRows.filter((f) => f.source_type === "
|
|
1731
|
+
const healCandidates = allFactsRows.filter((f) => f.source_type !== "immutable_document");
|
|
1732
|
+
const documentAnchors = allFactsRows.filter((f) => f.source_type === "immutable_document").map(({ id, title, source_ref }) => ({ id, title, source_ref }));
|
|
1667
1733
|
const userPrompt = `Heal Candidates:
|
|
1668
1734
|
${JSON.stringify(healCandidates.map((f) => {
|
|
1669
1735
|
const { embedding: _embedding, embedding_blob: _blob, ...rest } = f;
|
|
@@ -1706,7 +1772,7 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1706
1772
|
await this.db.runAsync(`
|
|
1707
1773
|
INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, created_at, updated_at)
|
|
1708
1774
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1709
|
-
`, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "
|
|
1775
|
+
`, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "librarian_inferred", now, now]);
|
|
1710
1776
|
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
1711
1777
|
}
|
|
1712
1778
|
});
|
|
@@ -1742,10 +1808,12 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1742
1808
|
throw new WikiBusyError("forget", entityId);
|
|
1743
1809
|
}
|
|
1744
1810
|
this.activeMaintenanceJobs.add(jobKey);
|
|
1811
|
+
this._notifyStatusSubscribers(entityId);
|
|
1745
1812
|
try {
|
|
1746
1813
|
await this._doRunLibrarian(entityId);
|
|
1747
1814
|
} finally {
|
|
1748
1815
|
this.activeMaintenanceJobs.delete(jobKey);
|
|
1816
|
+
this._notifyStatusSubscribers(entityId);
|
|
1749
1817
|
}
|
|
1750
1818
|
}
|
|
1751
1819
|
async runHeal(entityId) {
|
|
@@ -1766,10 +1834,12 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1766
1834
|
throw new WikiBusyError("forget", entityId);
|
|
1767
1835
|
}
|
|
1768
1836
|
this.activeMaintenanceJobs.add(jobKey);
|
|
1837
|
+
this._notifyStatusSubscribers(entityId);
|
|
1769
1838
|
try {
|
|
1770
1839
|
await this._doRunHeal(entityId);
|
|
1771
1840
|
} finally {
|
|
1772
1841
|
this.activeMaintenanceJobs.delete(jobKey);
|
|
1842
|
+
this._notifyStatusSubscribers(entityId);
|
|
1773
1843
|
}
|
|
1774
1844
|
}
|
|
1775
1845
|
async runReembed(entityId, opts) {
|
|
@@ -1909,6 +1979,40 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
1909
1979
|
heal: this.activeMaintenanceJobs.has(this._healKey(entityId))
|
|
1910
1980
|
};
|
|
1911
1981
|
}
|
|
1982
|
+
/**
|
|
1983
|
+
* Subscribe to {@link EntityStatus} changes for a single entity. The callback
|
|
1984
|
+
* is invoked synchronously once with the current status before this method
|
|
1985
|
+
* returns, then again on every transition where any of `ingesting`,
|
|
1986
|
+
* `librarian`, or `heal` flips. No polling, no duplicate snapshots.
|
|
1987
|
+
*
|
|
1988
|
+
* Returns an idempotent unsubscribe function.
|
|
1989
|
+
*
|
|
1990
|
+
* See also {@link getEntityStatus} for a synchronous point-in-time read.
|
|
1991
|
+
*/
|
|
1992
|
+
subscribeEntityStatus(entityId, callback) {
|
|
1993
|
+
const initial = this.getEntityStatus(entityId);
|
|
1994
|
+
let set = this.statusSubscribers.get(entityId);
|
|
1995
|
+
if (!set) {
|
|
1996
|
+
set = /* @__PURE__ */ new Set();
|
|
1997
|
+
this.statusSubscribers.set(entityId, set);
|
|
1998
|
+
}
|
|
1999
|
+
const entry = { callback, last: this._copyEntityStatus(initial) };
|
|
2000
|
+
set.add(entry);
|
|
2001
|
+
try {
|
|
2002
|
+
callback(this._copyEntityStatus(initial));
|
|
2003
|
+
} catch (err) {
|
|
2004
|
+
console.error(`[WikiMemory.subscribeEntityStatus] callback error for entityId="${entityId}" during initial emission`, err);
|
|
2005
|
+
}
|
|
2006
|
+
let active = true;
|
|
2007
|
+
return () => {
|
|
2008
|
+
if (!active) return;
|
|
2009
|
+
active = false;
|
|
2010
|
+
const s = this.statusSubscribers.get(entityId);
|
|
2011
|
+
if (!s) return;
|
|
2012
|
+
s.delete(entry);
|
|
2013
|
+
if (s.size === 0) this.statusSubscribers.delete(entityId);
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
1912
2016
|
clearVectorCache() {
|
|
1913
2017
|
this.vectorCache.clear();
|
|
1914
2018
|
}
|
|
@@ -2006,6 +2110,7 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
2006
2110
|
this.activeMaintenanceJobs.add(this._importKey(entityId));
|
|
2007
2111
|
}
|
|
2008
2112
|
try {
|
|
2113
|
+
await this.assertNoLegacySourceTypes();
|
|
2009
2114
|
for (const [entityId, bundle] of Object.entries(dump.entities)) {
|
|
2010
2115
|
await this._doImportEntity(entityId, bundle, merge);
|
|
2011
2116
|
}
|
|
@@ -2059,6 +2164,10 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
2059
2164
|
}
|
|
2060
2165
|
}
|
|
2061
2166
|
for (const fact of bundle.facts) {
|
|
2167
|
+
const sourceType = this._normalizeImportedSourceType(String(fact.source_type), {
|
|
2168
|
+
entityId,
|
|
2169
|
+
factId: fact.id
|
|
2170
|
+
});
|
|
2062
2171
|
const tagsJson = JSON.stringify(Array.isArray(fact.tags) ? fact.tags : []);
|
|
2063
2172
|
const safeUpdatedAt = Number.isFinite(fact.updated_at) ? fact.updated_at : 0;
|
|
2064
2173
|
const existing = existingFactsById.get(fact.id);
|
|
@@ -2107,14 +2216,14 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
2107
2216
|
if (blobData != null) {
|
|
2108
2217
|
await this.db.runAsync(
|
|
2109
2218
|
`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 = ?`,
|
|
2110
|
-
[entityId, fact.title, fact.body, tagsJson, fact.confidence,
|
|
2219
|
+
[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]
|
|
2111
2220
|
);
|
|
2112
2221
|
factsWithPreservedBlob.set(fact.id, blobData);
|
|
2113
2222
|
if (!fact.deleted_at) preservedBlobDims.add(blobData.byteLength / 4);
|
|
2114
2223
|
} else {
|
|
2115
2224
|
await this.db.runAsync(
|
|
2116
2225
|
`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 = ?`,
|
|
2117
|
-
[entityId, fact.title, fact.body, tagsJson, fact.confidence,
|
|
2226
|
+
[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]
|
|
2118
2227
|
);
|
|
2119
2228
|
}
|
|
2120
2229
|
existingFactsById.set(fact.id, { id: fact.id, entity_id: entityId, updated_at: safeUpdatedAt });
|
|
@@ -2124,14 +2233,14 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
2124
2233
|
if (blobData != null) {
|
|
2125
2234
|
await this.db.runAsync(
|
|
2126
2235
|
`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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2127
|
-
[fact.id, entityId, fact.title, fact.body, tagsJson, fact.confidence,
|
|
2236
|
+
[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]
|
|
2128
2237
|
);
|
|
2129
2238
|
factsWithPreservedBlob.set(fact.id, blobData);
|
|
2130
2239
|
if (!fact.deleted_at) preservedBlobDims.add(blobData.byteLength / 4);
|
|
2131
2240
|
} else {
|
|
2132
2241
|
await this.db.runAsync(
|
|
2133
2242
|
`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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2134
|
-
[fact.id, entityId, fact.title, fact.body, tagsJson, fact.confidence,
|
|
2243
|
+
[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]
|
|
2135
2244
|
);
|
|
2136
2245
|
}
|
|
2137
2246
|
existingFactsById.set(fact.id, { id: fact.id, entity_id: entityId, updated_at: safeUpdatedAt });
|
|
@@ -2429,6 +2538,7 @@ The following document anchors are provided for contradiction detection only. Do
|
|
|
2429
2538
|
throw new WikiBusyError("forget", entityId);
|
|
2430
2539
|
}
|
|
2431
2540
|
this.activeIngestJobs.add(jobKey);
|
|
2541
|
+
this._notifyStatusSubscribers(entityId);
|
|
2432
2542
|
try {
|
|
2433
2543
|
const { chunks, truncated } = chunkText(params.documentChunk, maxChunkLength, chunkOverlap);
|
|
2434
2544
|
if (chunks.length === 0) {
|
|
@@ -2478,7 +2588,7 @@ ${chunk}`;
|
|
|
2478
2588
|
await this.db.runAsync(
|
|
2479
2589
|
`INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, source_hash, source_ref, created_at, updated_at)
|
|
2480
2590
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2481
|
-
[id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "
|
|
2591
|
+
[id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "immutable_document", sourceHash, sourceRef, now, now]
|
|
2482
2592
|
);
|
|
2483
2593
|
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
2484
2594
|
}
|
|
@@ -2500,6 +2610,7 @@ ${chunk}`;
|
|
|
2500
2610
|
return { truncated, chunks: chunks.length };
|
|
2501
2611
|
} finally {
|
|
2502
2612
|
this.activeIngestJobs.delete(jobKey);
|
|
2613
|
+
this._notifyStatusSubscribers(entityId);
|
|
2503
2614
|
}
|
|
2504
2615
|
}
|
|
2505
2616
|
};
|
|
@@ -2743,5 +2854,6 @@ exports.WikiMemory = WikiMemory;
|
|
|
2743
2854
|
exports.createWiki = createWiki;
|
|
2744
2855
|
exports.formatContext = formatContext;
|
|
2745
2856
|
exports.formatMemoryDump = formatMemoryDump;
|
|
2857
|
+
exports.parseEmbedding = parseEmbedding;
|
|
2746
2858
|
//# sourceMappingURL=index.js.map
|
|
2747
2859
|
//# sourceMappingURL=index.js.map
|