@equationalapplications/core-llm-wiki 7.7.7 → 7.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { BaseRepository, WikiGraphNodeOwnershipConflict, __privateAdd, MetadataRepository, EmbeddingService, SearchService, JobManager, PromptService, resolveGrounding, IngestionService, MaintenanceService, ImportExportService, RetrievalService, WriteService, __privateGet, __privateSet, normalizeSourceRef, normalizeSourceHash, entitySummaryMetaKey, WikiSourceRefHashCollision, DiagnosticBuffer, validateManifest, WikiDraftNotFound, generateId, emptyManifest, resolveNodeType, WikiStrictOntologyViolation, validateInlineEdges, resolveEdgeDefinitions, typeSatisfies, normalizeTitleKey, edgeTripleAllowed, WikiTransactionError, extractSqliteCode } from './chunk-46X3HO77.mjs';
2
- export { DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, DEFAULT_MAX_EMBED_CHARS, EMBED_CHARS_CEILING, HEAL_ANCHORS_PER_CANDIDATE, HEAL_BATCH_SIZE, HEAL_MAX_FACT_BODY_CHARS_L3, HEAL_MAX_TASKS, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, WikiBusyError, WikiDraftNotFound, WikiDuplicateHashError, WikiGraphNodeOwnershipConflict, WikiIngestEmptyError, WikiInvalidReadOptions, WikiParseError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiTransactionError, chunkText, configureRandomSource, parseEmbedding, safeSlice, validateManifest } from './chunk-46X3HO77.mjs';
1
+ import { BaseRepository, WikiGraphNodeOwnershipConflict, liveAtSql, __privateAdd, MetadataRepository, EmbeddingService, SearchService, TemporalService, JobManager, PromptService, resolveGrounding, IngestionService, MaintenanceService, ImportExportService, RetrievalService, WriteService, __privateGet, __privateSet, normalizeSourceRef, normalizeSourceHash, entitySummaryMetaKey, WikiSourceRefHashCollision, DiagnosticBuffer, validateManifest, WikiDraftNotFound, generateId, emptyManifest, resolveNodeType, WikiStrictOntologyViolation, validateInlineEdges, resolveEdgeDefinitions, typeSatisfies, normalizeTitleKey, edgeTripleAllowed, MAX_EMBED_ATTEMPTS, UsageMeter, WikiBusyError, WikiInvalidReadOptions, isLiveAt, packFactsByBudget, factTokenCost, WikiTransactionError, extractSqliteCode } from './chunk-M76CF2Q6.mjs';
2
+ export { DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, DEFAULT_MAX_EMBED_CHARS, EMBED_CHARS_CEILING, HEAL_ANCHORS_PER_CANDIDATE, HEAL_BATCH_SIZE, HEAL_MAX_FACT_BODY_CHARS_L3, HEAL_MAX_TASKS, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, WikiBusyError, WikiDraftNotFound, WikiDuplicateHashError, WikiGraphNodeOwnershipConflict, WikiIngestEmptyError, WikiInvalidReadOptions, WikiParseError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiSupersedeError, WikiTransactionError, chunkText, configureRandomSource, parseEmbedding, safeSlice, validateManifest } from './chunk-M76CF2Q6.mjs';
3
3
  import { deriveTrustTier, isStaleAfter, appendRelatedSection, buildConceptDocument, buildLogMd, buildEntityIndexMd, buildRootIndexMd, isAllowedOkfPath, parseRootIndexMd, parseEntityIndexMd, parseConcept, splitRelatedSection, extractMarkdownLinks, parseLogMd, parseEventIdComment, latestVerified, formatSourcesJson, appendEventIdComment, parseVerifiedFlexible, parseCitationsList } from '@equationalapplications/core-okf';
4
4
 
5
5
  // src/db/schema.ts
@@ -35,7 +35,11 @@ async function setupDatabase(db, prefix) {
35
35
  okf_usage_window TEXT,
36
36
  embedding_failed_at INTEGER,
37
37
  embedding_failure_kind TEXT,
38
- embedding_attempts INTEGER NOT NULL DEFAULT 0
38
+ embedding_attempts INTEGER NOT NULL DEFAULT 0,
39
+ valid_from INTEGER,
40
+ valid_to INTEGER,
41
+ superseded_by TEXT,
42
+ superseded_at INTEGER
39
43
  );
40
44
 
41
45
  CREATE INDEX IF NOT EXISTS ${prefix}entries_entity_idx ON ${prefix}entries(entity_id);
@@ -102,7 +106,8 @@ async function setupDatabase(db, prefix) {
102
106
  event_type TEXT NOT NULL,
103
107
  summary TEXT NOT NULL,
104
108
  related_entry_id TEXT,
105
- created_at INTEGER NOT NULL
109
+ created_at INTEGER NOT NULL,
110
+ occurred_at INTEGER
106
111
  );
107
112
 
108
113
  CREATE INDEX IF NOT EXISTS ${prefix}events_entity_idx ON ${prefix}events(entity_id, created_at DESC);
@@ -110,7 +115,9 @@ async function setupDatabase(db, prefix) {
110
115
  CREATE TABLE IF NOT EXISTS ${prefix}checkpoints (
111
116
  entity_id TEXT PRIMARY KEY,
112
117
  heal_checkpoint INTEGER NOT NULL DEFAULT 0,
113
- memory_checkpoint INTEGER NOT NULL DEFAULT 0
118
+ memory_checkpoint INTEGER NOT NULL DEFAULT 0,
119
+ librarian_watermark_at INTEGER,
120
+ librarian_watermark_id TEXT
114
121
  );
115
122
 
116
123
  CREATE TABLE IF NOT EXISTS ${prefix}entity_manifests (
@@ -142,6 +149,7 @@ async function setupDatabase(db, prefix) {
142
149
  ON ${prefix}outbox (created_at);
143
150
  `);
144
151
  await createOkfV02IndexesIfColumnsExist(db, prefix);
152
+ await createTemporalIndexesIfColumnsExist(db, prefix);
145
153
  }
146
154
  async function createOkfV02IndexesIfColumnsExist(db, prefix) {
147
155
  for (const table of ["entries", "tasks"]) {
@@ -160,6 +168,20 @@ async function createOkfV02IndexesIfColumnsExist(db, prefix) {
160
168
  }
161
169
  }
162
170
  }
171
+ async function createTemporalIndexesIfColumnsExist(db, prefix) {
172
+ const cols = await db.getAllAsync(`PRAGMA table_info(${prefix}entries)`);
173
+ const names = new Set(cols.map((c) => c.name));
174
+ if (names.has("superseded_by")) {
175
+ await db.execAsync(
176
+ `CREATE INDEX IF NOT EXISTS ${prefix}entries_superseded_idx ON ${prefix}entries(entity_id, superseded_by) WHERE superseded_by IS NOT NULL;`
177
+ );
178
+ }
179
+ if (names.has("valid_from") && names.has("valid_to")) {
180
+ await db.execAsync(
181
+ `CREATE INDEX IF NOT EXISTS ${prefix}entries_temporal_idx ON ${prefix}entries(entity_id) WHERE valid_from IS NOT NULL OR valid_to IS NOT NULL;`
182
+ );
183
+ }
184
+ }
163
185
 
164
186
  // src/db/serializedAdapter.ts
165
187
  function isDriverError(err) {
@@ -449,6 +471,35 @@ var MIGRATIONS = [
449
471
  DROP INDEX IF EXISTS ${prefix}edges_entity_idx;
450
472
  `);
451
473
  }
474
+ },
475
+ {
476
+ version: 13,
477
+ description: "Temporal facts: valid_from/valid_to/superseded_by/superseded_at on entries, occurred_at on events, librarian watermark on checkpoints",
478
+ run: async (db, prefix) => {
479
+ const adds = [
480
+ ["entries", "valid_from", "INTEGER"],
481
+ ["entries", "valid_to", "INTEGER"],
482
+ ["entries", "superseded_by", "TEXT"],
483
+ ["entries", "superseded_at", "INTEGER"],
484
+ ["events", "occurred_at", "INTEGER"],
485
+ ["checkpoints", "librarian_watermark_at", "INTEGER"],
486
+ ["checkpoints", "librarian_watermark_id", "TEXT"]
487
+ ];
488
+ const existingByTable = /* @__PURE__ */ new Map();
489
+ for (const [table, column, type] of adds) {
490
+ let existing = existingByTable.get(table);
491
+ if (!existing) {
492
+ const cols = await db.getAllAsync(`PRAGMA table_info(${prefix}${table})`);
493
+ existing = new Set(cols.map((c) => c.name));
494
+ existingByTable.set(table, existing);
495
+ }
496
+ if (!existing.has(column)) {
497
+ await db.execAsync(`ALTER TABLE ${prefix}${table} ADD COLUMN ${column} ${type}`);
498
+ existing.add(column);
499
+ }
500
+ }
501
+ await createTemporalIndexesIfColumnsExist(db, prefix);
502
+ }
452
503
  }
453
504
  ];
454
505
  for (let i = 1; i < MIGRATIONS.length; i++) {
@@ -523,7 +574,13 @@ function mapRowToFact(row) {
523
574
  last_verified_by: row.last_verified_by ?? null,
524
575
  // Spec §2.7 + §5.3: hydrate so read() consumers don't re-call the helpers.
525
576
  isStale: isStaleAfter(staleAfterRaw, now),
526
- trustTier: deriveTrustTier(okf_verified)
577
+ trustTier: deriveTrustTier(okf_verified),
578
+ // Temporal (spec 2026-09-29 §4, §10.8): keys appear only when set so
579
+ // existing deep-equality assertions and host snapshots are unchanged.
580
+ ...row.valid_from != null ? { valid_from: Number(row.valid_from) } : {},
581
+ ...row.valid_to != null ? { valid_to: Number(row.valid_to) } : {},
582
+ ...row.superseded_by != null ? { superseded_by: String(row.superseded_by) } : {},
583
+ ...row.superseded_at != null ? { superseded_at: Number(row.superseded_at) } : {}
527
584
  };
528
585
  }
529
586
  function normalizeEmbeddingBlobValue(blob) {
@@ -944,12 +1001,14 @@ var _EntryRepository = class _EntryRepository extends BaseRepository {
944
1001
  */
945
1002
  async findHealCandidatesByEntityId(entityId, limit, recheckCutoff, tx) {
946
1003
  const executor = this.getExecutor(tx);
1004
+ const now = Date.now();
947
1005
  const rows = await executor.getAllAsync(
948
1006
  `SELECT * FROM ${this.prefix}entries
949
1007
  WHERE entity_id = ? AND deleted_at IS NULL AND source_type != 'immutable_document'
1008
+ AND ${liveAtSql("current")}
950
1009
  AND (heal_checked_at IS NULL OR heal_checked_at <= ?)
951
1010
  ORDER BY updated_at ASC LIMIT ?`,
952
- [entityId, recheckCutoff, limit]
1011
+ [entityId, now, now, recheckCutoff, limit]
953
1012
  );
954
1013
  return rows.map(mapRowToFact);
955
1014
  }
@@ -963,6 +1022,7 @@ var _EntryRepository = class _EntryRepository extends BaseRepository {
963
1022
  const executor = this.getExecutor(tx);
964
1023
  const columns = opts?.withBody ? "id, title, source_ref, body, lifecycle_status" : "id, title, source_ref";
965
1024
  const rows = [];
1025
+ const now = Date.now();
966
1026
  for (let i = 0; i < ids.length; i += this.chunkSize) {
967
1027
  const chunk = ids.slice(i, i + this.chunkSize);
968
1028
  const placeholders = chunk.map(() => "?").join(", ");
@@ -970,8 +1030,9 @@ var _EntryRepository = class _EntryRepository extends BaseRepository {
970
1030
  `SELECT ${columns} FROM ${this.prefix}entries
971
1031
  WHERE entity_id = ? AND deleted_at IS NULL
972
1032
  AND source_type = 'immutable_document'
1033
+ AND ${liveAtSql("current")}
973
1034
  AND id IN (${placeholders})`,
974
- [entityId, ...chunk]
1035
+ [entityId, now, now, ...chunk]
975
1036
  );
976
1037
  rows.push(...chunkRows);
977
1038
  }
@@ -991,10 +1052,11 @@ var _EntryRepository = class _EntryRepository extends BaseRepository {
991
1052
  */
992
1053
  async findRecentByEntityId(entityId, limit, tx, excludeSourceRef) {
993
1054
  const executor = this.getExecutor(tx);
1055
+ const now = Date.now();
994
1056
  const excludeClause = excludeSourceRef ? ` AND (source_ref IS NULL OR source_ref != ?)` : "";
995
- const args = excludeSourceRef ? [entityId, excludeSourceRef, limit] : [entityId, limit];
1057
+ const args = excludeSourceRef ? [entityId, now, now, excludeSourceRef, limit] : [entityId, now, now, limit];
996
1058
  const rows = await executor.getAllAsync(
997
- `SELECT * FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL${excludeClause} ORDER BY updated_at DESC LIMIT ?`,
1059
+ `SELECT * FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL AND ${liveAtSql("current")}${excludeClause} ORDER BY updated_at DESC LIMIT ?`,
998
1060
  args
999
1061
  );
1000
1062
  return rows.map(mapRowToFact);
@@ -1202,6 +1264,97 @@ var _EntryRepository = class _EntryRepository extends BaseRepository {
1202
1264
  }, tx);
1203
1265
  }
1204
1266
  }
1267
+ /** Spec §4: write any subset of the four temporal columns. Absent key = untouched; null = clear. */
1268
+ async setTemporal(id, entityId, fields, tx, opts) {
1269
+ const keys = ["valid_from", "valid_to", "superseded_by", "superseded_at"].filter((k) => k in fields);
1270
+ if (keys.length === 0) return 0;
1271
+ const executor = this.getExecutor(tx);
1272
+ const result = await executor.runAsync(
1273
+ `UPDATE ${this.prefix}entries SET ${keys.map((k) => `${k} = ?`).join(", ")} WHERE id = ? AND entity_id = ?`,
1274
+ [...keys.map((k) => fields[k] ?? null), id, entityId]
1275
+ );
1276
+ if (result.changes > 0 && opts?.outbox !== false) {
1277
+ const payload = { id, entity_id: entityId };
1278
+ for (const k of keys) payload[k] = fields[k] ?? null;
1279
+ await this.outbox.push({ entityId, tableName: "entries", recordId: id, operation: "UPDATE", payload }, tx);
1280
+ }
1281
+ return result.changes;
1282
+ }
1283
+ async findTemporalRow(id, tx) {
1284
+ const executor = this.getExecutor(tx);
1285
+ const row = await executor.getFirstAsync(
1286
+ `SELECT id, entity_id, source_type, deleted_at, created_at, valid_from, valid_to, superseded_by
1287
+ FROM ${this.prefix}entries WHERE id = ?`,
1288
+ [id]
1289
+ );
1290
+ if (!row) return null;
1291
+ return {
1292
+ id: String(row.id),
1293
+ entity_id: String(row.entity_id),
1294
+ source_type: row.source_type,
1295
+ deleted_at: row.deleted_at != null ? Number(row.deleted_at) : null,
1296
+ created_at: Number(row.created_at),
1297
+ valid_from: row.valid_from != null ? Number(row.valid_from) : null,
1298
+ valid_to: row.valid_to != null ? Number(row.valid_to) : null,
1299
+ superseded_by: row.superseded_by != null ? String(row.superseded_by) : null
1300
+ };
1301
+ }
1302
+ /**
1303
+ * Ids of non-deleted facts NOT live at `t` (spec §4.2). In 'current' mode a
1304
+ * row with both validity columns NULL is always live, so the query is
1305
+ * restricted to rows that carry a validity column — served by the partial
1306
+ * entries_temporal_idx.
1307
+ */
1308
+ async findNonLiveIdsByEntityIds(entityIds, mode, t, tx) {
1309
+ if (entityIds.length === 0) return /* @__PURE__ */ new Set();
1310
+ const executor = this.getExecutor(tx);
1311
+ const placeholders = entityIds.map(() => "?").join(",");
1312
+ const temporalOnly = mode === "current" ? " AND (valid_from IS NOT NULL OR valid_to IS NOT NULL)" : "";
1313
+ const rows = await executor.getAllAsync(
1314
+ `SELECT id FROM ${this.prefix}entries
1315
+ WHERE entity_id IN (${placeholders}) AND deleted_at IS NULL${temporalOnly}
1316
+ AND NOT ${liveAtSql(mode)}`,
1317
+ [...entityIds, t, t]
1318
+ );
1319
+ return new Set(rows.map((r) => r.id));
1320
+ }
1321
+ async findSupersessionChainIds(entityId, factId, maxDepth, tx) {
1322
+ const executor = this.getExecutor(tx);
1323
+ const successors = await executor.getAllAsync(
1324
+ `WITH RECURSIVE fwd(id, depth) AS (
1325
+ SELECT superseded_by, 1 FROM ${this.prefix}entries WHERE id = ? AND entity_id = ? AND superseded_by IS NOT NULL
1326
+ UNION
1327
+ SELECT e.superseded_by, fwd.depth + 1 FROM fwd
1328
+ JOIN ${this.prefix}entries e ON e.id = fwd.id AND e.entity_id = ?
1329
+ WHERE e.superseded_by IS NOT NULL AND fwd.depth < ?
1330
+ )
1331
+ SELECT id FROM fwd ORDER BY depth ASC`,
1332
+ [factId, entityId, entityId, maxDepth]
1333
+ );
1334
+ const predecessors = await executor.getAllAsync(
1335
+ `WITH RECURSIVE bwd(id, depth) AS (
1336
+ SELECT id, 1 FROM ${this.prefix}entries WHERE entity_id = ? AND superseded_by = ?
1337
+ UNION
1338
+ SELECT e.id, bwd.depth + 1 FROM bwd
1339
+ JOIN ${this.prefix}entries e ON e.entity_id = ? AND e.superseded_by = bwd.id
1340
+ WHERE bwd.depth < ?
1341
+ )
1342
+ SELECT id FROM bwd ORDER BY depth DESC`,
1343
+ [entityId, factId, entityId, maxDepth]
1344
+ );
1345
+ const drop = (ids) => ids.map((r) => r.id).filter((id) => id !== factId);
1346
+ return { predecessors: drop(predecessors), successors: drop(successors) };
1347
+ }
1348
+ async findExpiredIds(entityId, cutoff, tx) {
1349
+ const executor = this.getExecutor(tx);
1350
+ const rows = await executor.getAllAsync(
1351
+ `SELECT id FROM ${this.prefix}entries
1352
+ WHERE entity_id = ? AND deleted_at IS NULL AND valid_to IS NOT NULL AND valid_to <= ?
1353
+ ORDER BY valid_to ASC`,
1354
+ [entityId, cutoff]
1355
+ );
1356
+ return rows.map((r) => r.id);
1357
+ }
1205
1358
  /**
1206
1359
  * Bulk soft-delete all entries for an entity.
1207
1360
  * Stages DELETE outbox entries for each row in the same transaction.
@@ -1634,9 +1787,11 @@ var _EntryRepository = class _EntryRepository extends BaseRepository {
1634
1787
  const executor = this.getExecutor(tx);
1635
1788
  const placeholders = entityIds.map(() => "?").join(",");
1636
1789
  const draftClause = opts?.excludeDrafts === true ? ` AND lifecycle_status != 'draft'` : "";
1790
+ const liveClause = opts?.live ? ` AND ${liveAtSql(opts.live.mode)}` : "";
1791
+ const liveArgs = opts?.live ? [opts.live.t, opts.live.t] : [];
1637
1792
  const rows = await executor.getAllAsync(
1638
- `SELECT * FROM ${this.prefix}entries WHERE entity_id IN (${placeholders}) AND deleted_at IS NULL${draftClause} ORDER BY updated_at DESC LIMIT ?`,
1639
- [...entityIds, limit]
1793
+ `SELECT * FROM ${this.prefix}entries WHERE entity_id IN (${placeholders}) AND deleted_at IS NULL${draftClause}${liveClause} ORDER BY updated_at DESC LIMIT ?`,
1794
+ [...entityIds, ...liveArgs, limit]
1640
1795
  );
1641
1796
  return rows.map(mapRowToFact);
1642
1797
  }
@@ -1751,13 +1906,15 @@ var _EntryRepository = class _EntryRepository extends BaseRepository {
1751
1906
  /** Counts live mutable facts: eligible (past cooldown) vs deferred (in cooldown). */
1752
1907
  async countHealCandidatesByEntityId(entityId, recheckCutoff, tx) {
1753
1908
  const executor = this.getExecutor(tx);
1909
+ const now = Date.now();
1754
1910
  const row = await executor.getFirstAsync(
1755
1911
  `SELECT
1756
1912
  SUM(CASE WHEN heal_checked_at IS NULL OR heal_checked_at <= ? THEN 1 ELSE 0 END) AS eligible,
1757
1913
  SUM(CASE WHEN heal_checked_at IS NOT NULL AND heal_checked_at > ? THEN 1 ELSE 0 END) AS deferred
1758
1914
  FROM ${this.prefix}entries
1759
- WHERE entity_id = ? AND deleted_at IS NULL AND source_type != 'immutable_document'`,
1760
- [recheckCutoff, recheckCutoff, entityId]
1915
+ WHERE entity_id = ? AND deleted_at IS NULL AND source_type != 'immutable_document'
1916
+ AND ${liveAtSql("current")}`,
1917
+ [recheckCutoff, recheckCutoff, entityId, now, now]
1761
1918
  );
1762
1919
  return { eligible: Number(row?.eligible ?? 0), deferred: Number(row?.deferred ?? 0) };
1763
1920
  }
@@ -1800,6 +1957,21 @@ var _EntryRepository = class _EntryRepository extends BaseRepository {
1800
1957
  );
1801
1958
  return rows.map((r) => ({ id: r.id, title: r.title }));
1802
1959
  }
1960
+ /**
1961
+ * Count of live facts for `entityId` with no stored embedding that are still
1962
+ * retryable (`embedding_attempts < maxAttempts`). Backs the deferred
1963
+ * maintenance pending report's `reembedPending` flag and the reembed job's
1964
+ * entity selection (PR-C 2026-09-29).
1965
+ */
1966
+ async countReembedPending(entityId, maxAttempts, tx) {
1967
+ const executor = this.getExecutor(tx);
1968
+ const row = await executor.getFirstAsync(
1969
+ `SELECT COUNT(*) AS n FROM ${this.prefix}entries
1970
+ WHERE entity_id = ? AND deleted_at IS NULL AND embedding_blob IS NULL AND embedding IS NULL AND embedding_attempts < ?`,
1971
+ [entityId, maxAttempts]
1972
+ );
1973
+ return Number(row?.n ?? 0);
1974
+ }
1803
1975
  };
1804
1976
  /**
1805
1977
  * Column list and liveness predicate shared by findMiniSearchRows and
@@ -2248,6 +2420,18 @@ var TaskRepository = class extends BaseRepository {
2248
2420
  };
2249
2421
 
2250
2422
  // src/repositories/EventRepository.ts
2423
+ var AFTER_SQL = "((created_at > ?) OR (created_at = ? AND id > ?))";
2424
+ function afterArgs(wm) {
2425
+ return [wm.at, wm.at, wm.id];
2426
+ }
2427
+ function mapEvent(row) {
2428
+ if (row.occurred_at == null) {
2429
+ const rest = { ...row };
2430
+ delete rest.occurred_at;
2431
+ return rest;
2432
+ }
2433
+ return row;
2434
+ }
2251
2435
  var EventRepository = class extends BaseRepository {
2252
2436
  /**
2253
2437
  * Insert a new event row.
@@ -2256,30 +2440,32 @@ var EventRepository = class extends BaseRepository {
2256
2440
  async add(event, tx) {
2257
2441
  const executor = this.getExecutor(tx);
2258
2442
  await executor.runAsync(
2259
- `INSERT INTO ${this.prefix}events (id, entity_id, event_type, summary, related_entry_id, created_at)
2260
- VALUES (?, ?, ?, ?, ?, ?)`,
2443
+ `INSERT INTO ${this.prefix}events (id, entity_id, event_type, summary, related_entry_id, created_at, occurred_at)
2444
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
2261
2445
  [
2262
2446
  event.id,
2263
2447
  event.entity_id,
2264
2448
  event.event_type,
2265
2449
  event.summary,
2266
2450
  event.related_entry_id ?? null,
2267
- event.created_at
2451
+ event.created_at,
2452
+ event.occurred_at ?? null
2268
2453
  ]
2269
2454
  );
2270
2455
  }
2271
2456
  async addIgnoreDuplicate(event, tx) {
2272
2457
  const executor = this.getExecutor(tx);
2273
2458
  await executor.runAsync(
2274
- `INSERT OR IGNORE INTO ${this.prefix}events (id, entity_id, event_type, summary, related_entry_id, created_at)
2275
- VALUES (?, ?, ?, ?, ?, ?)`,
2459
+ `INSERT OR IGNORE INTO ${this.prefix}events (id, entity_id, event_type, summary, related_entry_id, created_at, occurred_at)
2460
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
2276
2461
  [
2277
2462
  event.id,
2278
2463
  event.entity_id,
2279
2464
  event.event_type,
2280
2465
  event.summary,
2281
2466
  event.related_entry_id ?? null,
2282
- event.created_at
2467
+ event.created_at,
2468
+ event.occurred_at ?? null
2283
2469
  ]
2284
2470
  );
2285
2471
  }
@@ -2288,10 +2474,11 @@ var EventRepository = class extends BaseRepository {
2288
2474
  * Defaults to a limit of 50.
2289
2475
  */
2290
2476
  async getRecent(entityId, limit = 50) {
2291
- return this.db.getAllAsync(
2477
+ const rows = await this.db.getAllAsync(
2292
2478
  `SELECT * FROM ${this.prefix}events WHERE entity_id = ? ORDER BY created_at DESC LIMIT ?`,
2293
2479
  [entityId, limit]
2294
2480
  );
2481
+ return rows.map(mapEvent);
2295
2482
  }
2296
2483
  /**
2297
2484
  * Return the most recent events for the given entity IDs, newest first.
@@ -2300,10 +2487,11 @@ var EventRepository = class extends BaseRepository {
2300
2487
  async getRecentForEntities(entityIds, limit = 50) {
2301
2488
  if (entityIds.length === 0) return [];
2302
2489
  const placeholders = entityIds.map(() => "?").join(", ");
2303
- return this.db.getAllAsync(
2490
+ const rows = await this.db.getAllAsync(
2304
2491
  `SELECT * FROM ${this.prefix}events WHERE entity_id IN (${placeholders}) ORDER BY created_at DESC LIMIT ?`,
2305
2492
  [...entityIds, limit]
2306
2493
  );
2494
+ return rows.map(mapEvent);
2307
2495
  }
2308
2496
  /**
2309
2497
  * Delete events for an entity that were created at or before the given cutoff timestamp.
@@ -2315,6 +2503,33 @@ var EventRepository = class extends BaseRepository {
2315
2503
  [entityId, cutoff]
2316
2504
  );
2317
2505
  }
2506
+ /**
2507
+ * Watermark-guarded prune (spec 2026-09-29 competitive-memory §6): delete
2508
+ * events at or before `cutoff` **and** at-or-before the librarian watermark.
2509
+ * Events the librarian has not processed yet are retention-held, never
2510
+ * deleted. `wm = null` (no watermark yet) deletes nothing.
2511
+ * Used by runPrune under the deferred/ops strategies.
2512
+ */
2513
+ async pruneThrough(entityId, cutoff, wm) {
2514
+ if (!wm) return { changes: 0 };
2515
+ return this.db.runAsync(
2516
+ `DELETE FROM ${this.prefix}events WHERE entity_id = ? AND created_at <= ? AND NOT ${AFTER_SQL}`,
2517
+ [entityId, cutoff, ...afterArgs(wm)]
2518
+ );
2519
+ }
2520
+ /**
2521
+ * Count events at or before `cutoff` that are after the watermark — the
2522
+ * retention-held tail `pruneThrough` left in place. `wm = null` counts all
2523
+ * events at or before `cutoff` (every one of them is held back).
2524
+ */
2525
+ async countHeldBack(entityId, cutoff, wm) {
2526
+ const where = wm ? ` AND ${AFTER_SQL}` : "";
2527
+ const row = await this.db.getFirstAsync(
2528
+ `SELECT COUNT(*) AS n FROM ${this.prefix}events WHERE entity_id = ? AND created_at <= ?${where}`,
2529
+ [entityId, cutoff, ...wm ? afterArgs(wm) : []]
2530
+ );
2531
+ return Number(row?.n ?? 0);
2532
+ }
2318
2533
  /**
2319
2534
  * Return the total number of events stored for an entity.
2320
2535
  * `tx` is optional — pass an active transaction handle for atomic reads.
@@ -2333,16 +2548,82 @@ var EventRepository = class extends BaseRepository {
2333
2548
  */
2334
2549
  async getByEntityId(entityId, limit) {
2335
2550
  if (limit != null) {
2336
- const rows = await this.db.getAllAsync(
2551
+ const rows2 = await this.db.getAllAsync(
2337
2552
  `SELECT * FROM ${this.prefix}events WHERE entity_id = ? ORDER BY created_at DESC LIMIT ?`,
2338
2553
  [entityId, limit]
2339
2554
  );
2340
- return rows.slice().reverse();
2555
+ return rows2.map(mapEvent).slice().reverse();
2341
2556
  }
2342
- return this.db.getAllAsync(
2557
+ const rows = await this.db.getAllAsync(
2343
2558
  `SELECT * FROM ${this.prefix}events WHERE entity_id = ? ORDER BY created_at ASC`,
2344
2559
  [entityId]
2345
2560
  );
2561
+ return rows.map(mapEvent);
2562
+ }
2563
+ /**
2564
+ * Return events for `entityId` strictly after the watermark `(at, id)` in
2565
+ * chronological (ASC, id-ASC tie-break) order. Pass `wm = null` to read
2566
+ * from the beginning of the entity's event log. `limit` caps the result.
2567
+ *
2568
+ * Used by the op-based librarian (PR-B) to enumerate its unprocessed tail.
2569
+ */
2570
+ async getAfter(entityId, wm, limit) {
2571
+ const limitClause = limit != null ? ` LIMIT ?` : "";
2572
+ const params = wm ? [entityId, wm.at, wm.at, wm.id] : [entityId];
2573
+ const where = wm ? `entity_id = ? AND ${AFTER_SQL}` : `entity_id = ?`;
2574
+ if (limit != null) params.push(limit);
2575
+ const rows = await this.db.getAllAsync(
2576
+ `SELECT * FROM ${this.prefix}events WHERE ${where} ORDER BY created_at ASC, id ASC${limitClause}`,
2577
+ params
2578
+ );
2579
+ return rows.map(mapEvent);
2580
+ }
2581
+ /**
2582
+ * Count of events for `entityId` strictly after the watermark. Used as
2583
+ * the op-based librarian's bounded batch size and by the deferred-maintenance
2584
+ * pending report. Pass `wm = null` to count the entity's whole event log
2585
+ * (no watermark yet) — mirrors `getAfter` / `sumSummaryCharsAfter`.
2586
+ */
2587
+ async countAfter(entityId, wm) {
2588
+ const params = wm ? [entityId, wm.at, wm.at, wm.id] : [entityId];
2589
+ const where = wm ? `entity_id = ? AND ${AFTER_SQL}` : `entity_id = ?`;
2590
+ const row = await this.db.getFirstAsync(
2591
+ `SELECT COUNT(*) as count FROM ${this.prefix}events WHERE ${where}`,
2592
+ params
2593
+ );
2594
+ return row?.count ?? 0;
2595
+ }
2596
+ /**
2597
+ * Return the event at a given chronological offset for `entityId` (0-based,
2598
+ * oldest first). Used by MaintenanceService.seedLibrarianWatermark to anchor
2599
+ * the watermark on a real row when a legacy DB first migrates to the
2600
+ * op-based librarian. The offset is clamped and truncated to defend against
2601
+ * hostile or fractional callers.
2602
+ */
2603
+ async getAtOffset(entityId, offset, tx) {
2604
+ const executor = this.getExecutor(tx);
2605
+ const row = await executor.getFirstAsync(
2606
+ `SELECT * FROM ${this.prefix}events WHERE entity_id = ? ORDER BY created_at ASC, id ASC LIMIT 1 OFFSET ?`,
2607
+ [entityId, Math.max(0, Math.trunc(offset))]
2608
+ );
2609
+ return row ? mapEvent(row) : null;
2610
+ }
2611
+ /**
2612
+ * Sum of `summary` lengths (in characters) for events for `entityId`
2613
+ * strictly after the watermark. Pass `wm = null` to sum the entity's whole
2614
+ * event log (no watermark yet). Used by the op-based librarian's token
2615
+ * budget estimator (PR-B) and by the auto-trigger's pending-size check
2616
+ * (PR-C). SQLite's `LENGTH` returns character count for UTF-8 text input.
2617
+ */
2618
+ async sumSummaryCharsAfter(entityId, wm, tx) {
2619
+ const executor = this.getExecutor(tx);
2620
+ const params = wm ? [entityId, wm.at, wm.at, wm.id] : [entityId];
2621
+ const where = wm ? `entity_id = ? AND ${AFTER_SQL}` : `entity_id = ?`;
2622
+ const row = await executor.getFirstAsync(
2623
+ `SELECT SUM(LENGTH(summary)) as total FROM ${this.prefix}events WHERE ${where}`,
2624
+ params
2625
+ );
2626
+ return row?.total ?? 0;
2346
2627
  }
2347
2628
  };
2348
2629
 
@@ -2471,6 +2752,7 @@ var EdgeRepository = class extends BaseRepository {
2471
2752
  ) >= ?
2472
2753
  AND n.source_type NOT IN (${excludeSourceTypesPlaceholders})
2473
2754
  AND (? = 0 OR n.lifecycle_status != 'draft')
2755
+ AND ${liveAtSql(opts.live.mode, "n.")}
2474
2756
  WHERE w.depth < ?
2475
2757
  AND instr(w.visited, ',' || (CASE WHEN e.source_id = w.node_id THEN e.target_id ELSE e.source_id END) || ',') = 0
2476
2758
  )
@@ -2491,6 +2773,8 @@ var EdgeRepository = class extends BaseRepository {
2491
2773
  minConfidenceRank,
2492
2774
  ...opts.excludeSourceTypes,
2493
2775
  opts.excludeDrafts === true ? 1 : 0,
2776
+ opts.live.t,
2777
+ opts.live.t,
2494
2778
  opts.maxDepth,
2495
2779
  opts.maxNodes
2496
2780
  ];
@@ -2520,6 +2804,186 @@ function mapRowToEdge(row) {
2520
2804
  };
2521
2805
  }
2522
2806
 
2807
+ // src/services/MaintenanceScheduler.ts
2808
+ var MaintenanceScheduler = class {
2809
+ constructor(db, options, maintenanceService, metadataRepo, eventRepo, entryRepo, jobManager) {
2810
+ this.db = db;
2811
+ this.options = options;
2812
+ this.maintenanceService = maintenanceService;
2813
+ this.metadataRepo = metadataRepo;
2814
+ this.eventRepo = eventRepo;
2815
+ this.entryRepo = entryRepo;
2816
+ this.jobManager = jobManager;
2817
+ }
2818
+ /**
2819
+ * Entities with **any** pending work only, sorted by `pendingEvents` desc
2820
+ * then `entityId` asc. Token estimate is ceil(pending summary chars / 4),
2821
+ * matching the auto-trigger's estimator (PR-B).
2822
+ */
2823
+ async getPending(entityIds) {
2824
+ const ids = entityIds ?? await this.metadataRepo.getDistinctEntityIds();
2825
+ const autoHealThreshold = this.options.config?.autoHealThreshold || 100;
2826
+ const canEmbed = typeof this.options.llmProvider.embed === "function";
2827
+ const out = [];
2828
+ for (const entityId of ids) {
2829
+ const wm = await this.metadataRepo.getLibrarianWatermark(entityId, this.db);
2830
+ const pendingEvents = await this.eventRepo.countAfter(entityId, wm);
2831
+ const chars = pendingEvents > 0 ? await this.eventRepo.sumSummaryCharsAfter(entityId, wm) : 0;
2832
+ const eventCount = await this.eventRepo.count(entityId);
2833
+ const cp = await this.metadataRepo.getCheckpoint(entityId, this.db);
2834
+ let healCheckpoint = cp.heal ?? 0;
2835
+ if (healCheckpoint > eventCount) healCheckpoint = 0;
2836
+ const healDue = eventCount - healCheckpoint >= autoHealThreshold;
2837
+ const reembedPending = canEmbed && await this.entryRepo.countReembedPending(entityId, MAX_EMBED_ATTEMPTS) > 0;
2838
+ if (pendingEvents > 0 || healDue || reembedPending) {
2839
+ out.push({ entityId, pendingEvents, pendingTokensEstimate: Math.ceil(chars / 4), healDue, reembedPending });
2840
+ }
2841
+ }
2842
+ return out.sort((x, y) => y.pendingEvents - x.pendingEvents || (x.entityId < y.entityId ? -1 : x.entityId > y.entityId ? 1 : 0));
2843
+ }
2844
+ /**
2845
+ * Execute pending maintenance (spec §6.2): fair-share rounds of at most one
2846
+ * unit per entity (librarian batch, else heal batch, else reembed), entities
2847
+ * ordered by `getPending` (pendingEvents desc, id asc). Holds the same
2848
+ * per-entity `JobManager` locks a manual run would; a busy entity is skipped
2849
+ * and reported, never thrown. Nothing is still running when this resolves.
2850
+ *
2851
+ * Stop precedence (spec §10 items 3-6): `deadline` > `budget_exhausted` >
2852
+ * `budget_too_small` > `complete`. `budget_too_small` means a job's smallest
2853
+ * unit needs more than the WHOLE budget — that job is skipped this run and
2854
+ * the run continues; `budget_exhausted` means a unit needs more than what
2855
+ * REMAINS — the run stops. The token budget meters `generateText` calls
2856
+ * only; embeddings are never metered.
2857
+ */
2858
+ async run(opts = {}) {
2859
+ if (opts.deadlineMs !== void 0 && (typeof opts.deadlineMs !== "number" || !Number.isFinite(opts.deadlineMs) || opts.deadlineMs < 0)) {
2860
+ throw new TypeError("deadlineMs must be a finite number >= 0");
2861
+ }
2862
+ const meter = new UsageMeter(opts.tokenBudget);
2863
+ const budget = opts.tokenBudget;
2864
+ const deadline = opts.deadlineMs !== void 0 ? Date.now() + Math.max(0, opts.deadlineMs) : Infinity;
2865
+ const jobs = new Set(opts.jobs ?? ["librarian", "heal", "reembed"]);
2866
+ const pending = await this.getPending(opts.entityIds);
2867
+ const states = pending.map((p) => ({
2868
+ p,
2869
+ report: { entityId: p.entityId, librarianPasses: 0, factsWritten: 0, healPasses: 0, reembedded: false },
2870
+ librarian: jobs.has("librarian") && p.pendingEvents > 0,
2871
+ heal: jobs.has("heal") && p.healDue,
2872
+ reembed: jobs.has("reembed") && p.reembedPending
2873
+ }));
2874
+ let stopped = { reason: "complete" };
2875
+ let tooSmall = null;
2876
+ const classify = (job, entityId, requiredEstimate) => {
2877
+ if (budget !== void 0 && requiredEstimate > budget) {
2878
+ tooSmall ?? (tooSmall = { reason: "budget_too_small", job, entityId, requiredEstimate });
2879
+ return "too_small";
2880
+ }
2881
+ return "exhausted";
2882
+ };
2883
+ const exhaustedStop = () => Date.now() >= deadline ? { reason: "deadline" } : { reason: "budget_exhausted" };
2884
+ const markBusy = (st, job) => {
2885
+ var _a;
2886
+ ((_a = st.report).busy ?? (_a.busy = [])).push(job);
2887
+ };
2888
+ rounds: for (; ; ) {
2889
+ let progressed = false;
2890
+ for (const st of states) {
2891
+ if (!st.librarian && !st.heal && !st.reembed) continue;
2892
+ if (Date.now() >= deadline) {
2893
+ stopped = { reason: "deadline" };
2894
+ break rounds;
2895
+ }
2896
+ const entityId = st.p.entityId;
2897
+ if (st.librarian) {
2898
+ if (this.jobManager.isBlocked("librarian", entityId)) {
2899
+ st.librarian = false;
2900
+ markBusy(st, "librarian");
2901
+ continue;
2902
+ }
2903
+ this.jobManager.acquireLock("librarian", entityId);
2904
+ let r;
2905
+ try {
2906
+ r = await this.maintenanceService.runLibrarianPass({ entityId, trigger: "call", meter });
2907
+ } finally {
2908
+ this.jobManager.releaseLock("librarian", entityId);
2909
+ }
2910
+ if (r.budgetStop) {
2911
+ if (classify("librarian", entityId, r.budgetStop.requiredEstimate) === "exhausted") {
2912
+ stopped = exhaustedStop();
2913
+ break rounds;
2914
+ }
2915
+ st.librarian = false;
2916
+ continue;
2917
+ }
2918
+ if (!r.processedThrough) {
2919
+ st.librarian = false;
2920
+ continue;
2921
+ }
2922
+ st.report.librarianPasses++;
2923
+ st.report.factsWritten += r.factsWritten;
2924
+ progressed = true;
2925
+ const wm = await this.metadataRepo.getLibrarianWatermark(entityId, this.db);
2926
+ if (await this.eventRepo.countAfter(entityId, wm) === 0) st.librarian = false;
2927
+ continue;
2928
+ }
2929
+ if (st.heal) {
2930
+ if (this.jobManager.isBlocked("heal", entityId)) {
2931
+ st.heal = false;
2932
+ markBusy(st, "heal");
2933
+ continue;
2934
+ }
2935
+ this.jobManager.acquireLock("heal", entityId);
2936
+ let h;
2937
+ try {
2938
+ h = await this.maintenanceService.doRunHeal(entityId, { trigger: "call", meter });
2939
+ } finally {
2940
+ this.jobManager.releaseLock("heal", entityId);
2941
+ }
2942
+ if (h.budgetStop && h.scanned === 0) {
2943
+ if (classify("heal", entityId, h.budgetStop.requiredEstimate) === "exhausted") {
2944
+ stopped = exhaustedStop();
2945
+ break rounds;
2946
+ }
2947
+ st.heal = false;
2948
+ continue;
2949
+ }
2950
+ st.report.healPasses++;
2951
+ progressed = true;
2952
+ if (h.remaining === 0) {
2953
+ st.heal = false;
2954
+ await this.metadataRepo.updateCheckpoint(entityId, { heal: await this.eventRepo.count(entityId) }, this.db);
2955
+ }
2956
+ if (h.budgetStop) {
2957
+ stopped = exhaustedStop();
2958
+ break rounds;
2959
+ }
2960
+ continue;
2961
+ }
2962
+ if (st.reembed) {
2963
+ st.reembed = false;
2964
+ try {
2965
+ await this.maintenanceService.runReembed(entityId);
2966
+ st.report.reembedded = true;
2967
+ progressed = true;
2968
+ } catch (err) {
2969
+ if (err instanceof WikiBusyError) markBusy(st, "reembed");
2970
+ else throw err;
2971
+ }
2972
+ }
2973
+ }
2974
+ if (!progressed) break;
2975
+ }
2976
+ if (stopped.reason === "complete" && tooSmall) stopped = tooSmall;
2977
+ return {
2978
+ perEntity: states.map((s) => s.report),
2979
+ tokensUsed: meter.used,
2980
+ estimated: meter.estimated,
2981
+ remaining: budget === void 0 ? null : meter.remaining,
2982
+ stoppedReason: stopped
2983
+ };
2984
+ }
2985
+ };
2986
+
2523
2987
  // src/prompts/ontology.ts
2524
2988
  var FACT_ONTOLOGY_FIELDS = `
2525
2989
  Each fact may optionally include:
@@ -2704,6 +3168,15 @@ var GraphTraversalService = class {
2704
3168
  const defaultMaxNodes = Number.isFinite(rawConfigDefault) && rawConfigDefault >= 1 ? Math.floor(rawConfigDefault) : fallbackMaxNodes;
2705
3169
  const rawMaxNodes = options.maxTraversalNodes ?? defaultMaxNodes;
2706
3170
  const maxNodes = Number.isFinite(rawMaxNodes) && rawMaxNodes >= 1 ? Math.floor(rawMaxNodes) : defaultMaxNodes;
3171
+ const rawAsOf = options.asOf;
3172
+ if (rawAsOf !== void 0 && !(typeof rawAsOf === "number" && Number.isFinite(rawAsOf) && rawAsOf >= 0)) {
3173
+ throw new WikiInvalidReadOptions("asOf", "must be a finite epoch-ms number >= 0");
3174
+ }
3175
+ const rawTokenBudget = options.tokenBudget;
3176
+ if (rawTokenBudget !== void 0 && !(typeof rawTokenBudget === "number" && Number.isFinite(rawTokenBudget) && rawTokenBudget >= 0)) {
3177
+ throw new WikiInvalidReadOptions("tokenBudget", "must be a finite number >= 0");
3178
+ }
3179
+ const live = rawAsOf === void 0 ? { mode: "current", t: Date.now() } : { mode: "asOf", t: Math.trunc(rawAsOf) };
2707
3180
  const opts = {
2708
3181
  maxDepth: Math.max(1, Math.min(options.maxDepth ?? 1, 3)),
2709
3182
  direction: options.direction ?? this.config.traversalDirection ?? "both",
@@ -2711,16 +3184,32 @@ var GraphTraversalService = class {
2711
3184
  minConfidence: options.minTraversalConfidence ?? this.config.minTraversalConfidence ?? "tentative",
2712
3185
  excludeSourceTypes: options.excludeSourceTypes ?? this.config.excludeSourceTypes ?? [],
2713
3186
  excludeDrafts: options.excludeDrafts ?? this.config.excludeDrafts ?? false,
3187
+ live,
2714
3188
  maxNodes
2715
3189
  };
2716
3190
  const { nodeIds, edges } = await this.edgeRepo.getNeighborhood(entityId, options.sourceId, opts);
2717
3191
  if (nodeIds.length === 0) return { nodes: [], edges: [] };
3192
+ const [anchor] = await this.entryRepo.findByIds([options.sourceId], [entityId]);
3193
+ if (anchor && !isLiveAt(anchor, live.mode, live.t)) return { nodes: [anchor], edges: [] };
2718
3194
  const nodes = await this.entryRepo.findByIds(nodeIds, [entityId]);
2719
3195
  const hydratedIds = new Set(nodes.map((node) => node.id));
2720
3196
  const filteredEdges = edges.filter(
2721
3197
  (edge) => hydratedIds.has(edge.source_id) && hydratedIds.has(edge.target_id)
2722
3198
  );
2723
- return { nodes, edges: filteredEdges };
3199
+ if (options.tokenBudget === void 0) return { nodes, edges: filteredEdges };
3200
+ const budget = Math.trunc(options.tokenBudget);
3201
+ const [anchorPacked] = packFactsByBudget([nodes[0]], budget).items;
3202
+ const kept = [anchorPacked];
3203
+ let used = factTokenCost(anchorPacked);
3204
+ for (const node of nodes.slice(1)) {
3205
+ const cost = factTokenCost(node);
3206
+ if (used + cost <= budget) {
3207
+ kept.push(node);
3208
+ used += cost;
3209
+ }
3210
+ }
3211
+ const keptIds = new Set(kept.map((n) => n.id));
3212
+ return { nodes: kept, edges: filteredEdges.filter((e) => keptIds.has(e.source_id) && keptIds.has(e.target_id)) };
2724
3213
  }
2725
3214
  };
2726
3215
 
@@ -3021,6 +3510,7 @@ var WikiMemory = class {
3021
3510
  this.lintService = new LintService(this.lintRepo, this.ontologyService);
3022
3511
  this.embeddingService = new EmbeddingService(this.db, this.options, this.entryRepo, this.metadataRepo);
3023
3512
  this.searchService = new SearchService(this.entryRepo);
3513
+ this.temporalService = new TemporalService(this.db, this.options, this.entryRepo, this.searchService, this.embeddingService);
3024
3514
  this.jobManager = new JobManager(this.prefix);
3025
3515
  this.promptService = new PromptService(options.config?.prompts, resolveGrounding(options.config?.grounding));
3026
3516
  this.ingestionService = new IngestionService(
@@ -3052,6 +3542,15 @@ var WikiMemory = class {
3052
3542
  this.promptService,
3053
3543
  this.ontologyService
3054
3544
  );
3545
+ this.maintenanceScheduler = new MaintenanceScheduler(
3546
+ this.db,
3547
+ this.options,
3548
+ this.maintenanceService,
3549
+ this.metadataRepo,
3550
+ this.eventRepo,
3551
+ this.entryRepo,
3552
+ this.jobManager
3553
+ );
3055
3554
  this.importExportService = new ImportExportService(
3056
3555
  this.db,
3057
3556
  this.entryRepo,
@@ -3102,11 +3601,13 @@ var WikiMemory = class {
3102
3601
  importExportService: this.importExportService,
3103
3602
  ingestionService: this.ingestionService,
3104
3603
  maintenanceService: this.maintenanceService,
3604
+ maintenanceScheduler: this.maintenanceScheduler,
3105
3605
  retrievalService: this.retrievalService,
3106
3606
  searchService: this.searchService,
3107
3607
  writeService: this.writeService,
3108
3608
  promptService: this.promptService,
3109
3609
  graphTraversalService: this.graphTraversalService,
3610
+ temporalService: this.temporalService,
3110
3611
  entryRepo: this.entryRepo,
3111
3612
  sourceRefIndexRepo: this.sourceRefIndexRepo,
3112
3613
  metadataRepo: this.metadataRepo,
@@ -3324,6 +3825,23 @@ var WikiMemory = class {
3324
3825
  async write(entityId, event) {
3325
3826
  return this.writeService.write(entityId, event);
3326
3827
  }
3828
+ /** Await auto-mode background librarian/heal jobs started by write(). No-op in 'deferred' mode. */
3829
+ async drain() {
3830
+ return this.writeService.drain();
3831
+ }
3832
+ /**
3833
+ * Report which entities have pending maintenance work (librarian events
3834
+ * behind the watermark, heal backlog, unembedded facts) without running
3835
+ * anything. Entities with no pending work are omitted; sorted by
3836
+ * `pendingEvents` desc then `entityId` asc.
3837
+ */
3838
+ async getPendingMaintenance(entityIds) {
3839
+ return this.maintenanceScheduler.getPending(entityIds);
3840
+ }
3841
+ /** Run pending librarian/heal/reembed work under an optional token budget and deadline (spec §6.2). Nothing runs after it resolves. */
3842
+ async runPendingMaintenance(options) {
3843
+ return this.maintenanceScheduler.run(options);
3844
+ }
3327
3845
  /**
3328
3846
  * @param options.promptOverride - Applies only to this manual call. Does NOT affect
3329
3847
  * WriteService-triggered auto-runs. For persistent prompt customization across auto-runs,
@@ -3397,6 +3915,18 @@ var WikiMemory = class {
3397
3915
  async forget(entityId, params, opts) {
3398
3916
  return this.maintenanceService.forget(entityId, params, opts);
3399
3917
  }
3918
+ /**
3919
+ * End `oldId`'s validity and link it to its replacement (spec §4.3). One
3920
+ * transaction. Throws `WikiSupersedeError` for missing, foreign, already
3921
+ * superseded, immutable-document, or cyclic targets.
3922
+ */
3923
+ async supersede(entityId, oldId, replacement, options) {
3924
+ return this.temporalService.supersede(entityId, oldId, replacement, options);
3925
+ }
3926
+ /** The supersession chain containing `factId`, oldest first. [] when unknown. */
3927
+ async history(entityId, factId) {
3928
+ return this.temporalService.history(entityId, factId);
3929
+ }
3400
3930
  /**
3401
3931
  * Full and partial ingestion both reject foreign-owned generated fact IDs with
3402
3932
  * `WikiGraphNodeOwnershipConflict`. Such collisions are unexpected. Ingestion
@@ -3715,10 +4245,11 @@ function formatContext(bundle, options) {
3715
4245
  maxFacts: options?.maxFacts ?? 10,
3716
4246
  maxTasks: options?.maxTasks ?? 10,
3717
4247
  maxEvents: options?.maxEvents ?? 10,
3718
- includeConfidence: options?.includeConfidence ?? true,
3719
- includeTags: options?.includeTags ?? true,
3720
- includeEntityIds: options?.includeEntityIds ?? false,
3721
- includeFactScores: options?.includeFactScores ?? false,
4248
+ includeConfidence: !(options?.compact ?? false) && (options?.includeConfidence ?? true),
4249
+ includeTags: !(options?.compact ?? false) && (options?.includeTags ?? true),
4250
+ includeEntityIds: !(options?.compact ?? false) && (options?.includeEntityIds ?? false),
4251
+ includeFactScores: !(options?.compact ?? false) && (options?.includeFactScores ?? false),
4252
+ compact: options?.compact ?? false,
3722
4253
  factWeights: {
3723
4254
  confidence: options?.factWeights?.confidence ?? 1,
3724
4255
  accessCount: options?.factWeights?.accessCount ?? 0.3,
@@ -4009,7 +4540,12 @@ function factFrontmatterV2(f) {
4009
4540
  // `verified` key", not from "empty `verified` list").
4010
4541
  ...f.okf_verified && f.okf_verified.length > 0 ? { verified: f.okf_verified } : {},
4011
4542
  ...f.okf_sources && f.okf_sources.length > 0 ? { sources: f.okf_sources } : {},
4012
- ...f.okf_usage_window ? { usage_window: f.okf_usage_window } : {}
4543
+ ...f.okf_usage_window ? { usage_window: f.okf_usage_window } : {},
4544
+ // Temporal (spec 2026-09-29 §10.9): flat epoch-ms keys, like created_at.
4545
+ ...f.valid_from != null ? { valid_from: f.valid_from } : {},
4546
+ ...f.valid_to != null ? { valid_to: f.valid_to } : {},
4547
+ ...f.superseded_by != null ? { superseded_by: f.superseded_by } : {},
4548
+ ...f.superseded_at != null ? { superseded_at: f.superseded_at } : {}
4013
4549
  };
4014
4550
  }
4015
4551
  function taskFrontmatterV2(t) {
@@ -4337,9 +4873,16 @@ function frontmatterToFact(entityId, id, frontmatter, body, now) {
4337
4873
  okf_verified: verified,
4338
4874
  okf_usage_window: usageWindow,
4339
4875
  last_verified_at: lastV?.at ?? null,
4340
- last_verified_by: lastV?.by ?? null
4876
+ last_verified_by: lastV?.by ?? null,
4877
+ ...optionalEpoch("valid_from", frontmatter.valid_from),
4878
+ ...optionalEpoch("valid_to", frontmatter.valid_to),
4879
+ ...typeof frontmatter.superseded_by === "string" ? { superseded_by: frontmatter.superseded_by } : {},
4880
+ ...optionalEpoch("superseded_at", frontmatter.superseded_at)
4341
4881
  };
4342
4882
  }
4883
+ function optionalEpoch(key, v) {
4884
+ return typeof v === "number" && Number.isFinite(v) && v >= 0 ? { [key]: Math.trunc(v) } : {};
4885
+ }
4343
4886
  function frontmatterToTask(entityId, id, frontmatter, now, usesV1StatusRule) {
4344
4887
  const created_at = parseFrontmatterTimestamp(frontmatter.created_at, now);
4345
4888
  const updated_at = parseFrontmatterTimestamp(