@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 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
- source_type: 'user_stated' | 'agent_inferred' | 'user_confirmed' | 'user_document';
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
- source_type: 'user_stated' | 'agent_inferred' | 'user_confirmed' | 'user_document';
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 'agent_inferred',
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 < ? AND id IN (${placeholders})`,
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.runLibrarianThenMaybeHeal(entityId, count).catch(console.error).finally(() => this.activeMaintenanceJobs.delete(jobKey));
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 !== "agent_inferred") continue;
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, "agent_inferred", now, now]);
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 < ? AND source_type != 'user_document' AND deleted_at IS NULL
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 < ? OR (last_accessed_at IS NULL AND created_at < ?)) AND source_type != 'user_document' AND deleted_at IS NULL
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 !== "user_document");
1666
- const documentAnchors = allFactsRows.filter((f) => f.source_type === "user_document").map(({ id, title, source_ref }) => ({ id, title, source_ref }));
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, "agent_inferred", now, now]);
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, fact.source_type, fact.source_hash, fact.source_ref, fact.created_at, safeUpdatedAt, fact.last_accessed_at, fact.access_count, fact.deleted_at, blobData, fact.id]
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, fact.source_type, fact.source_hash, fact.source_ref, fact.created_at, safeUpdatedAt, fact.last_accessed_at, fact.access_count, fact.deleted_at, fact.id]
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, fact.source_type, fact.source_hash, fact.source_ref, fact.created_at, safeUpdatedAt, fact.last_accessed_at, fact.access_count, fact.deleted_at, blobData]
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, fact.source_type, fact.source_hash, fact.source_ref, fact.created_at, safeUpdatedAt, fact.last_accessed_at, fact.access_count, fact.deleted_at]
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, "user_document", sourceHash, sourceRef, now, now]
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