@equationalapplications/core-llm-wiki 7.7.6 → 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/README.md +85 -0
- package/dist/{chunk-PBUZLSOM.mjs → chunk-M76CF2Q6.mjs} +1193 -189
- package/dist/chunk-M76CF2Q6.mjs.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1837 -289
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +575 -32
- package/dist/index.mjs.map +1 -1
- package/dist/{testing-BIFNLUva.d.mts → testing-CWZaXKLj.d.mts} +560 -4
- package/dist/{testing-BIFNLUva.d.ts → testing-CWZaXKLj.d.ts} +560 -4
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1142 -201
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +1 -1
- package/package.json +2 -2
- package/dist/chunk-PBUZLSOM.mjs.map +0 -1
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2555
|
+
return rows2.map(mapEvent).slice().reverse();
|
|
2341
2556
|
}
|
|
2342
|
-
|
|
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(
|