@equationalapplications/core-llm-wiki 7.7.7 → 7.9.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.
@@ -176,6 +176,15 @@ var WikiDraftNotFound = class _WikiDraftNotFound extends Error {
176
176
  Object.setPrototypeOf(this, _WikiDraftNotFound.prototype);
177
177
  }
178
178
  };
179
+ var WikiSupersedeError = class _WikiSupersedeError extends Error {
180
+ constructor(reason) {
181
+ super(`supersede() rejected: ${reason}`);
182
+ this.code = "WIKI_SUPERSEDE_REJECTED";
183
+ this.name = "WikiSupersedeError";
184
+ this.reason = reason;
185
+ Object.setPrototypeOf(this, _WikiSupersedeError.prototype);
186
+ }
187
+ };
179
188
 
180
189
  // src/utils/ids.ts
181
190
  var _injectedGetRandomValues = null;
@@ -569,6 +578,16 @@ var _SearchService = class _SearchService {
569
578
  this.staleEntities.add(entityId);
570
579
  this.staleEpochs.set(entityId, (this.staleEpochs.get(entityId) ?? 0) + 1);
571
580
  }
581
+ /**
582
+ * Runs syncEntries(id, []) for each entity marked stale when this is called,
583
+ * so only those entities are rebuilt. Each rebuild still takes its own turn
584
+ * on the sync chain, and like syncEntries this never rejects. With nothing
585
+ * stale it resolves at once, without waiting on work already on the chain.
586
+ */
587
+ async syncStale() {
588
+ const ids = [...this.staleEntities];
589
+ await Promise.all(ids.map((id) => this.syncEntries(id, [])));
590
+ }
572
591
  /**
573
592
  * Clears the parsed vector cache. Useful for mid-loop flush guarantees
574
593
  * or memory pressure evictions.
@@ -1745,6 +1764,21 @@ function groundingEvidenceBlock(writer, cfg) {
1745
1764
  return `EVIDENCE REQUIREMENT: every object in "${key}" must also carry an "evidence" array of 1 to ${cfg.maxEvidence} quotes. Each quote must be an exact substring copied character-for-character from ${source} (the SOURCE section), at least ${cfg.minEvidenceChars} characters long. Do not paraphrase. Do not quote these instructions, the ontology manifest, or any existing fact. A fact whose quotes cannot be found in the SOURCE section is stored as an unreviewed draft.
1746
1765
  "evidence": ["exact substring copied from the SOURCE section"]`;
1747
1766
  }
1767
+ var OPS_EXTRACT_SYSTEM_PROMPT = `You are a memory extraction agent. Read the numbered events and extract durable facts about the user or entity, and actionable tasks.
1768
+ Return ONLY a valid JSON object matching this schema:
1769
+ {
1770
+ "facts": [{ "title": "string (max 80 chars)", "body": "string (max 800 chars)", "tags": ["string"], "confidence": "certain|inferred|tentative", "source_event": "the event label, e.g. e1", "valid_from": "optional ISO-8601 date the fact became true, only when the events state it" }],
1771
+ "tasks": [{ "description": "string", "priority": "number (0-10)" }]
1772
+ }
1773
+ One fact per claim. Every fact must name the single event it comes from in "source_event". Do not return markdown, just raw JSON.`;
1774
+ var OPS_RESOLVE_SYSTEM_PROMPT = `You are a memory reconciliation agent. Each item has a NEW candidate fact and EXISTING facts labelled n1, n2, ... that look similar to it. Choose exactly one op per item:
1775
+ - "ADD": the candidate is new information; the existing facts are about something else.
1776
+ - "UPDATE": one existing fact states the same thing and the candidate adds detail. Give the merged "title" and "body" for that existing fact.
1777
+ - "SUPERSEDE": the candidate replaces an existing fact that is no longer true (moved, changed jobs, changed preference, reversed decision).
1778
+ - "NOOP": an existing fact already states the candidate.
1779
+ Return ONLY a valid JSON object matching this schema:
1780
+ { "ops": [{ "item": 0, "op": "ADD|UPDATE|SUPERSEDE|NOOP", "target": "n1", "title": "UPDATE only", "body": "UPDATE only" }] }
1781
+ "target" is required for UPDATE, SUPERSEDE and NOOP and must be one of that item's own labels. Do not return markdown, just raw JSON.`;
1748
1782
 
1749
1783
  // src/utils/healConstants.ts
1750
1784
  var HEAL_MAX_ANCHORS = 50;
@@ -1983,6 +2017,22 @@ ${JSON.stringify(facts, null, 2)}`
1983
2017
  ${JSON.stringify(facts, null, 2)}`
1984
2018
  };
1985
2019
  }
2020
+ /** Ops extract (spec §5.2): events only — no existing facts. */
2021
+ buildOpsExtractPrompt(events, ontologyContext) {
2022
+ const lines = events.map((l) => `${l.label} [${new Date(l.at).toISOString()}] (${l.event.event_type}) ${l.event.summary}`);
2023
+ const grounding = this.groundingFor("librarian");
2024
+ return {
2025
+ systemPrompt: this.appendGrounding(this.appendOntology(OPS_EXTRACT_SYSTEM_PROMPT, ontologyContext), "librarian"),
2026
+ userPrompt: `Events:
2027
+ ${lines.join("\n")}`,
2028
+ ...grounding ? { groundingCorpus: buildGroundingCorpus(events.map((l) => l.event.summary)) } : {}
2029
+ };
2030
+ }
2031
+ /** Ops resolve (spec §5.4): only each item's own neighbours. */
2032
+ buildOpsResolvePrompt(items) {
2033
+ return { systemPrompt: OPS_RESOLVE_SYSTEM_PROMPT, userPrompt: `Items:
2034
+ ${JSON.stringify({ items })}` };
2035
+ }
1986
2036
  /**
1987
2037
  * The system prompt each writer would send, without hydrating data (spec
1988
2038
  * §8.3). `buildSystemPrompt` with no variables hydrates only ontology
@@ -2061,7 +2111,14 @@ var SEVERITY = {
2061
2111
  grounding_missing: "warn",
2062
2112
  grounding_failed: "warn",
2063
2113
  classification_low_confidence: "info",
2064
- classification_invalid: "warn"
2114
+ classification_invalid: "warn",
2115
+ llm_usage: "info",
2116
+ librarian_gate: "info",
2117
+ librarian_op_rejected: "warn",
2118
+ contradicts_document: "warn",
2119
+ resolve_failed: "warn",
2120
+ read_budget: "info",
2121
+ event_retention_held: "info"
2065
2122
  };
2066
2123
  var MESSAGE = {
2067
2124
  ingest_chunk_failed: "One or more document chunks failed to parse or generate.",
@@ -2076,7 +2133,14 @@ var MESSAGE = {
2076
2133
  grounding_missing: "A fact carried no qualifying evidence and was stored as a draft.",
2077
2134
  grounding_failed: "A fact carried evidence not found in its source and was stored as a draft.",
2078
2135
  classification_low_confidence: "A classifier answer was below the confidence threshold and was not applied.",
2079
- classification_invalid: "A classifier answer was invalid and was not applied."
2136
+ classification_invalid: "A classifier answer was invalid and was not applied.",
2137
+ llm_usage: "An LLM call completed; token usage attached.",
2138
+ librarian_gate: "Librarian gate outcome counts for one pass.",
2139
+ librarian_op_rejected: "A librarian reconciliation op was invalid and the candidate was added instead.",
2140
+ contradicts_document: "A candidate contradicted a document fact and was stored as a draft.",
2141
+ resolve_failed: "Reconciliation failed for a candidate; it was stored as a draft.",
2142
+ read_budget: "A read was packed to a token budget.",
2143
+ event_retention_held: "Event pruning was held back because the librarian has not processed those events yet."
2080
2144
  };
2081
2145
  var warnedNonFunction = /* @__PURE__ */ new WeakSet();
2082
2146
  function copyDetail(detail) {
@@ -2162,6 +2226,79 @@ function edgeDropDiagnostic(drop, ctx) {
2162
2226
  return { code: "edge_dropped", operation: ctx.operation, trigger: ctx.trigger, entityId: ctx.entityId, detail };
2163
2227
  }
2164
2228
 
2229
+ // src/utils/usage.ts
2230
+ function estimateTokens(text) {
2231
+ return Math.ceil(text.length / 4);
2232
+ }
2233
+ var UsageMeter = class {
2234
+ constructor(budget) {
2235
+ this.used = 0;
2236
+ this.estimated = false;
2237
+ if (budget !== void 0 && (typeof budget !== "number" || !Number.isFinite(budget) || budget < 0)) {
2238
+ throw new TypeError("tokenBudget must be a finite number >= 0");
2239
+ }
2240
+ this.budget = budget;
2241
+ }
2242
+ get remaining() {
2243
+ return this.budget === void 0 ? Infinity : Math.max(0, this.budget - this.used);
2244
+ }
2245
+ fits(estimate) {
2246
+ return estimate <= this.remaining;
2247
+ }
2248
+ record(r) {
2249
+ this.used += r.inputTokens + r.outputTokens;
2250
+ if (r.estimated) this.estimated = true;
2251
+ }
2252
+ };
2253
+ var WikiBudgetExhausted = class _WikiBudgetExhausted extends Error {
2254
+ constructor(requiredEstimate, remaining) {
2255
+ super(`LLM call needs ~${requiredEstimate} input tokens; ${remaining} remain in the budget.`);
2256
+ this.name = "WikiBudgetExhausted";
2257
+ this.requiredEstimate = requiredEstimate;
2258
+ this.remaining = remaining;
2259
+ Object.setPrototypeOf(this, _WikiBudgetExhausted.prototype);
2260
+ }
2261
+ };
2262
+
2263
+ // src/utils/llmCall.ts
2264
+ function validUsage(u) {
2265
+ if (!u || typeof u !== "object") return false;
2266
+ const { inputTokens, outputTokens } = u;
2267
+ return [inputTokens, outputTokens].every((n) => typeof n === "number" && Number.isFinite(n) && n >= 0);
2268
+ }
2269
+ async function callLlm(options, ctx, prompts) {
2270
+ const promptEstimate = estimateTokens(prompts.systemPrompt) + estimateTokens(prompts.userPrompt);
2271
+ if (ctx.meter && !ctx.meter.fits(promptEstimate)) {
2272
+ throw new WikiBudgetExhausted(promptEstimate, ctx.meter.remaining);
2273
+ }
2274
+ const provider = options.llmProvider;
2275
+ const params = { systemPrompt: prompts.systemPrompt, userPrompt: prompts.userPrompt };
2276
+ let text;
2277
+ let reported;
2278
+ if (typeof provider.generateTextWithUsage === "function") {
2279
+ const result = await provider.generateTextWithUsage(params);
2280
+ if (!result || typeof result.text !== "string") {
2281
+ throw new TypeError("generateTextWithUsage() must resolve to { text: string, usage? }");
2282
+ }
2283
+ text = result.text;
2284
+ reported = validUsage(result.usage) ? result.usage : void 0;
2285
+ } else {
2286
+ text = await provider.generateText(params);
2287
+ }
2288
+ const record = reported ? { inputTokens: reported.inputTokens, outputTokens: reported.outputTokens, estimated: false } : { inputTokens: promptEstimate, outputTokens: estimateTokens(text), estimated: true };
2289
+ ctx.meter?.record(record);
2290
+ if (options.config?.reportLlmUsage === true) {
2291
+ emitDiagnostic(options, {
2292
+ code: "llm_usage",
2293
+ operation: ctx.operation,
2294
+ trigger: ctx.trigger,
2295
+ entityId: ctx.entityId,
2296
+ detail: { inputTokens: record.inputTokens, outputTokens: record.outputTokens, estimated: record.estimated }
2297
+ });
2298
+ }
2299
+ return text;
2300
+ }
2301
+
2165
2302
  // src/services/IngestionService.ts
2166
2303
  var LLM_PASS_DIAGNOSTIC_CODES = ["ingest_chunk_failed", "fact_rejected", "fact_deduplicated"];
2167
2304
  function dedupDiagnostic(entityId, sourceRef, locator) {
@@ -2238,7 +2375,7 @@ var IngestionService = class {
2238
2375
  ontologyContext
2239
2376
  );
2240
2377
  try {
2241
- const responseText = await this.options.llmProvider.generateText({ systemPrompt, userPrompt });
2378
+ const responseText = await callLlm(this.options, { operation: "ingest", entityId, trigger: "call" }, { systemPrompt, userPrompt });
2242
2379
  const result2 = parseJsonResponse(responseText);
2243
2380
  const rawFacts = Array.isArray(result2.facts) ? result2.facts : [];
2244
2381
  const facts = [];
@@ -2908,6 +3045,37 @@ var MetadataRepository = class extends BaseRepository {
2908
3045
  [entityId]
2909
3046
  );
2910
3047
  }
3048
+ /**
3049
+ * Read the op-based librarian's high-water mark. Returns `null` when the
3050
+ * entity has never had its watermark set.
3051
+ */
3052
+ async getLibrarianWatermark(entityId, tx) {
3053
+ const executor = this.getExecutor(tx);
3054
+ const row = await executor.getFirstAsync(
3055
+ `SELECT librarian_watermark_at, librarian_watermark_id FROM ${this.prefix}checkpoints WHERE entity_id = ?`,
3056
+ [entityId]
3057
+ );
3058
+ if (!row || row.librarian_watermark_at == null || row.librarian_watermark_id == null) {
3059
+ return null;
3060
+ }
3061
+ return { at: row.librarian_watermark_at, id: row.librarian_watermark_id };
3062
+ }
3063
+ /**
3064
+ * Write the op-based librarian's high-water mark. Uses `ON CONFLICT`
3065
+ * `DO UPDATE` so existing `memory_checkpoint` / `heal_checkpoint` rows
3066
+ * are preserved verbatim.
3067
+ */
3068
+ async setLibrarianWatermark(entityId, wm, tx) {
3069
+ const executor = this.getExecutor(tx);
3070
+ await executor.runAsync(
3071
+ `INSERT INTO ${this.prefix}checkpoints (entity_id, memory_checkpoint, heal_checkpoint, librarian_watermark_at, librarian_watermark_id)
3072
+ VALUES (?, 0, 0, ?, ?)
3073
+ ON CONFLICT(entity_id) DO UPDATE SET
3074
+ librarian_watermark_at = excluded.librarian_watermark_at,
3075
+ librarian_watermark_id = excluded.librarian_watermark_id`,
3076
+ [entityId, wm.at, wm.id]
3077
+ );
3078
+ }
2911
3079
  // META TABLE METHODS
2912
3080
  async getMeta(key, tx) {
2913
3081
  const executor = this.getExecutor(tx);
@@ -3080,6 +3248,8 @@ async function runBatched(args) {
3080
3248
  const results = [];
3081
3249
  const skipped = [];
3082
3250
  let batches = 0;
3251
+ const settled = /* @__PURE__ */ new Set();
3252
+ let budgetStop;
3083
3253
  let batchSize = initialBatchSize(maxOutputTokens);
3084
3254
  const trim = async (candidate) => {
3085
3255
  const whole = await buildPrompt(candidate);
@@ -3113,6 +3283,7 @@ async function runBatched(args) {
3113
3283
  } else {
3114
3284
  const reason = fromCall && !isTruncationError(err) && !isConfigError(err) ? "call_error" : "non_convergent";
3115
3285
  skipped.push({ item: batch[0], reason });
3286
+ settled.add(batch[0]);
3116
3287
  onSkip?.(batch[0], err);
3117
3288
  }
3118
3289
  return;
@@ -3121,7 +3292,7 @@ async function runBatched(args) {
3121
3292
  const mid = Math.ceil(batch.length / 2);
3122
3293
  if (mid < batchSize) batchSize = mid;
3123
3294
  let i = 0;
3124
- while (i < batch.length) {
3295
+ while (i < batch.length && !budgetStop) {
3125
3296
  const size = Math.min(batchSize, batch.length - i);
3126
3297
  const trimmed = await trim(batch.slice(i, i + size));
3127
3298
  await attempt(trimmed.batch, trimmed.prompts);
@@ -3129,6 +3300,7 @@ async function runBatched(args) {
3129
3300
  }
3130
3301
  };
3131
3302
  const attempt = async (batch, prebuilt, attemptLevel = 0) => {
3303
+ if (budgetStop) return;
3132
3304
  if (batch.length === 0) return;
3133
3305
  const prompts = prebuilt && attemptLevel === 0 ? prebuilt : await buildPrompt(batch, attemptLevel);
3134
3306
  batches++;
@@ -3136,6 +3308,10 @@ async function runBatched(args) {
3136
3308
  try {
3137
3309
  responseText = await call(prompts);
3138
3310
  } catch (err) {
3311
+ if (err instanceof WikiBudgetExhausted) {
3312
+ budgetStop = { requiredEstimate: err.requiredEstimate };
3313
+ return;
3314
+ }
3139
3315
  await onFailure(batch, err, attemptLevel, true);
3140
3316
  return;
3141
3317
  }
@@ -3147,19 +3323,781 @@ async function runBatched(args) {
3147
3323
  return;
3148
3324
  }
3149
3325
  results.push(result);
3326
+ for (const item of batch) settled.add(item);
3150
3327
  };
3151
3328
  let index = 0;
3152
- while (index < items.length) {
3329
+ while (index < items.length && !budgetStop) {
3153
3330
  const { batch, prompts } = await trim(items.slice(index, index + batchSize));
3154
3331
  index += batch.length;
3155
3332
  await attempt(batch, prompts, 0);
3156
3333
  }
3157
- return { results, skipped, batches };
3334
+ const unattempted = budgetStop ? items.filter((item) => !settled.has(item)) : [];
3335
+ return { results, skipped, batches, unattempted, ...budgetStop ? { budgetStop } : {} };
3158
3336
  }
3159
3337
 
3160
- // src/services/MaintenanceService.ts
3338
+ // src/services/librarian/constants.ts
3161
3339
  var FUZZY_THRESHOLD = 0.5;
3162
3340
  var MIN_TOKENS_TO_QUALIFY = 3;
3341
+
3342
+ // src/services/librarian/legacy.ts
3343
+ var runLegacyLibrarianPass = async (deps, ctx) => {
3344
+ const { entityId, trigger } = ctx;
3345
+ const promptOverride = ctx.promptOverride;
3346
+ const events = await deps.eventRepo.getRecent(entityId, 50);
3347
+ const currentFactsRows = await deps.entryRepo.findRecentByEntityId(entityId, 100);
3348
+ const currentFacts = currentFactsRows.map((f) => {
3349
+ const { embedding: _embedding, embedding_blob: _blob, ...rest } = f;
3350
+ return {
3351
+ ...rest,
3352
+ tags: typeof rest.tags === "string" ? JSON.parse(rest.tags) : rest.tags
3353
+ };
3354
+ });
3355
+ const ontologyContext = await deps.ontologyService?.buildPromptContext(entityId) ?? null;
3356
+ const promptEvents = events.reverse();
3357
+ const librarianGrounding = deps.promptService.groundingFor("librarian");
3358
+ const { systemPrompt, userPrompt, groundingCorpus: librarianCorpus = [] } = deps.promptService.buildLibrarianPrompt(
3359
+ promptEvents,
3360
+ currentFacts,
3361
+ promptOverride,
3362
+ ontologyContext
3363
+ );
3364
+ let responseText;
3365
+ try {
3366
+ responseText = await callLlm(
3367
+ deps.options,
3368
+ { operation: "librarian", entityId, trigger, meter: ctx.meter },
3369
+ { systemPrompt, userPrompt }
3370
+ );
3371
+ } catch (err) {
3372
+ if (err instanceof WikiBudgetExhausted) {
3373
+ return { processedThrough: null, factsWritten: 0, budgetStop: { requiredEstimate: err.requiredEstimate } };
3374
+ }
3375
+ throw err;
3376
+ }
3377
+ const result = parseJsonResponse(responseText);
3378
+ const facts = Array.isArray(result.facts) ? result.facts : [];
3379
+ const tasks = Array.isArray(result.tasks) ? result.tasks : [];
3380
+ const ontologyUpdates = result.ontology_updates;
3381
+ const diagBuffer = new DiagnosticBuffer();
3382
+ const diagBase = { entityId, operation: "librarian", trigger };
3383
+ const validFacts = [];
3384
+ const validFactItemIndexes = [];
3385
+ facts.forEach((raw, itemIndex) => {
3386
+ const valid = validateFact(raw);
3387
+ if (valid) {
3388
+ validFacts.push(valid);
3389
+ validFactItemIndexes.push(itemIndex);
3390
+ } else {
3391
+ diagBuffer.push({ ...diagBase, code: "fact_rejected", detail: { itemIndex, reason: factRejectionReason(raw) } });
3392
+ }
3393
+ });
3394
+ const validTasks = [];
3395
+ tasks.forEach((raw, itemIndex) => {
3396
+ const valid = validateTask(raw);
3397
+ if (valid) validTasks.push(valid);
3398
+ else diagBuffer.push({ ...diagBase, code: "task_rejected", detail: { itemIndex, reason: taskRejectionReason(raw) } });
3399
+ });
3400
+ const now = Date.now();
3401
+ const insertedFacts = [];
3402
+ await deps.db.withTransactionAsync(async (tx) => {
3403
+ let { mode, manifest } = await deps.ontologyService?.getEffectiveState(entityId, tx) ?? { mode: "off", manifest: { node_types: [], edge_types: [] } };
3404
+ if (mode === "emergent" && ontologyUpdates && deps.ontologyService) {
3405
+ manifest = await deps.ontologyService.mergeEmergentUpdates(entityId, ontologyUpdates, tx);
3406
+ }
3407
+ const titleIndex = /* @__PURE__ */ new Map();
3408
+ for (const existing of currentFactsRows) {
3409
+ titleIndex.set(normalizeTitleKey(existing.title), {
3410
+ id: existing.id,
3411
+ okf_type: existing.okf_type ?? null
3412
+ });
3413
+ }
3414
+ const factsForDedupe = await deps.entryRepo.findRecentByEntityId(entityId, 100, tx);
3415
+ const pendingEdges = [];
3416
+ for (const [k, fact] of validFacts.entries()) {
3417
+ const newTokens = titleTokens(fact.title);
3418
+ let skip = false;
3419
+ if (newTokens.size >= MIN_TOKENS_TO_QUALIFY) {
3420
+ for (const existing of factsForDedupe) {
3421
+ if (existing.source_type !== "librarian_inferred") continue;
3422
+ const existingTokens = titleTokens(existing.title);
3423
+ if (existingTokens.size >= MIN_TOKENS_TO_QUALIFY) {
3424
+ if (jaccardScore(newTokens, existingTokens) >= FUZZY_THRESHOLD) {
3425
+ skip = true;
3426
+ break;
3427
+ }
3428
+ }
3429
+ }
3430
+ }
3431
+ if (skip) {
3432
+ diagBuffer.push({ ...diagBase, code: "fact_deduplicated", detail: { itemIndex: validFactItemIndexes[k], reason: "fuzzy_title" } });
3433
+ continue;
3434
+ }
3435
+ const ontologyFact = fact;
3436
+ const id = generateId("fact_");
3437
+ const validationDrops = [];
3438
+ const normalized = deps.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false, drops: validationDrops }) ?? { okf_type: null, edges: [] };
3439
+ for (const drop of validationDrops) diagBuffer.push(edgeDropDiagnostic(drop, { ...diagBase, factId: id }));
3440
+ const grounding = librarianGrounding ? groundingOutcome(checkGrounding(fact.evidence, librarianCorpus, librarianGrounding), now) : null;
3441
+ if (grounding?.diagnostic) {
3442
+ diagBuffer.push({ ...diagBase, code: grounding.diagnostic.code, detail: { factId: id, itemIndex: validFactItemIndexes[k], reason: grounding.diagnostic.reason } });
3443
+ }
3444
+ const factObj = {
3445
+ id,
3446
+ entity_id: entityId,
3447
+ title: fact.title,
3448
+ body: fact.body,
3449
+ tags: fact.tags,
3450
+ confidence: fact.confidence,
3451
+ source_type: "librarian_inferred",
3452
+ source_hash: null,
3453
+ source_ref: null,
3454
+ created_at: now,
3455
+ updated_at: now,
3456
+ last_accessed_at: null,
3457
+ access_count: 0,
3458
+ deleted_at: null,
3459
+ okf_type: normalized.okf_type,
3460
+ ...grounding?.trust
3461
+ };
3462
+ await deps.entryRepo.upsert(factObj, tx);
3463
+ insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
3464
+ factsForDedupe.push(factObj);
3465
+ titleIndex.set(normalizeTitleKey(fact.title), { id, okf_type: normalized.okf_type });
3466
+ if (normalized.edges.length > 0) {
3467
+ pendingEdges.push({ sourceId: id, sourceType: normalized.okf_type, edges: normalized.edges });
3468
+ }
3469
+ }
3470
+ for (const item of pendingEdges) {
3471
+ const resolveDrops = [];
3472
+ await deps.ontologyService?.resolveAndPersistEdges(
3473
+ entityId,
3474
+ item.sourceId,
3475
+ item.sourceType,
3476
+ item.edges ?? [],
3477
+ manifest,
3478
+ titleIndex,
3479
+ tx,
3480
+ now,
3481
+ resolveDrops
3482
+ );
3483
+ for (const drop of resolveDrops) diagBuffer.push(edgeDropDiagnostic(drop, diagBase));
3484
+ }
3485
+ for (const task of validTasks) {
3486
+ const id = generateId("task_");
3487
+ const taskObj = {
3488
+ id,
3489
+ entity_id: entityId,
3490
+ description: task.description,
3491
+ status: "pending",
3492
+ priority: task.priority,
3493
+ created_at: now,
3494
+ updated_at: now,
3495
+ resolved_at: null,
3496
+ deleted_at: null
3497
+ };
3498
+ await deps.taskRepo.upsert(taskObj, tx);
3499
+ }
3500
+ });
3501
+ diagBuffer.flush(deps.options);
3502
+ await deps.searchService.syncEntries(entityId, insertedFacts.map((f) => f.id));
3503
+ for (const fact of insertedFacts) {
3504
+ await deps.embeddingService.embedFact(fact, { operation: "librarian", trigger });
3505
+ }
3506
+ deps.searchService.evictCache(entityId);
3507
+ const processedThrough = events.length > 0 ? { at: events[events.length - 1].created_at, id: events[events.length - 1].id } : null;
3508
+ const out = { processedThrough, factsWritten: insertedFacts.length };
3509
+ return out;
3510
+ };
3511
+
3512
+ // src/services/librarian/ops/text.ts
3513
+ var OPS_NEIGHBOUR_BODY_CHARS = 400;
3514
+ function normalizeFactText(title, body) {
3515
+ return `${title} ${body}`.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim();
3516
+ }
3517
+ function labelEvents(events) {
3518
+ return events.map((event, i) => ({ label: `e${i + 1}`, event, at: event.occurred_at ?? event.created_at }));
3519
+ }
3520
+ function parseValidFrom(v) {
3521
+ if (typeof v === "number") return Number.isFinite(v) && v >= 0 ? Math.trunc(v) : void 0;
3522
+ if (typeof v === "string") {
3523
+ const t = Date.parse(v);
3524
+ return Number.isFinite(t) && t >= 0 ? t : void 0;
3525
+ }
3526
+ return void 0;
3527
+ }
3528
+
3529
+ // src/services/librarian/ops/extract.ts
3530
+ var OPS_MAX_EVENTS_PER_PASS = 50;
3531
+ var OPS_MAX_EXTRACT_CHARS = 24e3;
3532
+ var RESOLVE_RESERVE_FACTOR = 2;
3533
+ var estimatePrompt = (p) => estimateTokens(p.systemPrompt) + estimateTokens(p.userPrompt);
3534
+ async function selectBatch(deps, entityId, wm, meter) {
3535
+ const events = await deps.eventRepo.getAfter(entityId, wm, OPS_MAX_EVENTS_PER_PASS);
3536
+ if (events.length === 0) return { batch: [] };
3537
+ let n = 0;
3538
+ let chars = 0;
3539
+ for (const e of events) {
3540
+ if (n > 0 && chars + e.summary.length > OPS_MAX_EXTRACT_CHARS) break;
3541
+ chars += e.summary.length;
3542
+ n++;
3543
+ }
3544
+ let batch = labelEvents(events.slice(0, n));
3545
+ if (meter && meter.budget !== void 0) {
3546
+ const ontologyContext = await deps.ontologyService?.buildPromptContext(entityId) ?? null;
3547
+ for (; ; ) {
3548
+ const need = RESOLVE_RESERVE_FACTOR * estimatePrompt(deps.promptService.buildOpsExtractPrompt(batch, ontologyContext));
3549
+ if (meter.fits(need)) break;
3550
+ if (batch.length === 1) return { budgetStop: { requiredEstimate: need } };
3551
+ batch = labelEvents(events.slice(0, Math.max(1, Math.floor(batch.length / 2))));
3552
+ }
3553
+ }
3554
+ return { batch };
3555
+ }
3556
+ async function runExtract(deps, ctx, batch) {
3557
+ const { entityId, trigger } = ctx;
3558
+ const ontologyContext = await deps.ontologyService?.buildPromptContext(entityId) ?? null;
3559
+ const prompt = deps.promptService.buildOpsExtractPrompt(batch, ontologyContext);
3560
+ const text = await callLlm(deps.options, { operation: "librarian", entityId, trigger, meter: ctx.meter }, prompt);
3561
+ const parsed = parseJsonResponse(text);
3562
+ const base = { entityId, operation: "librarian", trigger };
3563
+ const diagnostics = [];
3564
+ const byLabel = new Map(batch.map((l) => [l.label, l]));
3565
+ const newestAt = batch[batch.length - 1].at;
3566
+ const candidates = [];
3567
+ (Array.isArray(parsed.facts) ? parsed.facts : []).forEach((raw, index) => {
3568
+ const fact = validateFact(raw);
3569
+ if (!fact) {
3570
+ diagnostics.push({ ...base, code: "fact_rejected", detail: { itemIndex: index, reason: factRejectionReason(raw) } });
3571
+ return;
3572
+ }
3573
+ const r = raw;
3574
+ const sourceLabel = typeof r.source_event === "string" && byLabel.has(r.source_event) ? r.source_event : null;
3575
+ const validFrom = parseValidFrom(r.valid_from) ?? (sourceLabel ? byLabel.get(sourceLabel).at : newestAt);
3576
+ candidates.push({ index, fact, sourceLabel, validFrom });
3577
+ });
3578
+ const tasks = [];
3579
+ (Array.isArray(parsed.tasks) ? parsed.tasks : []).forEach((raw, index) => {
3580
+ const task = validateTask(raw);
3581
+ if (task) tasks.push(task);
3582
+ else diagnostics.push({ ...base, code: "task_rejected", detail: { itemIndex: index, reason: taskRejectionReason(raw) } });
3583
+ });
3584
+ return {
3585
+ batch,
3586
+ candidates,
3587
+ tasks,
3588
+ ...parsed.ontology_updates ? { ontologyUpdates: parsed.ontology_updates } : {},
3589
+ groundingCorpus: prompt.groundingCorpus ?? null,
3590
+ diagnostics
3591
+ };
3592
+ }
3593
+
3594
+ // src/services/librarian/ops/gate.ts
3595
+ var clamp01 = (v, d) => typeof v === "number" && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : d;
3596
+ function resolveGateConfig(cfg) {
3597
+ const kRaw = cfg?.k;
3598
+ const k = typeof kRaw === "number" && Number.isFinite(kRaw) ? Math.min(20, Math.max(1, Math.trunc(kRaw))) : 5;
3599
+ const dupThreshold = clamp01(cfg?.dupThreshold, 0.89);
3600
+ const novelThreshold = clamp01(cfg?.novelThreshold, 0.3);
3601
+ if (novelThreshold > dupThreshold) throw new TypeError("librarian.gate: novelThreshold must be <= dupThreshold");
3602
+ return { k, dupThreshold, novelThreshold };
3603
+ }
3604
+ function titlesMatch(a, b) {
3605
+ const ta = titleTokens(a);
3606
+ const tb = titleTokens(b);
3607
+ if (ta.size < MIN_TOKENS_TO_QUALIFY || tb.size < MIN_TOKENS_TO_QUALIFY) return false;
3608
+ return jaccardScore(ta, tb) >= FUZZY_THRESHOLD;
3609
+ }
3610
+ function classifyCandidate(candidate, neighbours, cfg, mode) {
3611
+ const key = normalizeFactText(candidate.title, candidate.body);
3612
+ const same = neighbours.find((n) => normalizeFactText(n.title, n.body) === key);
3613
+ if (same) return { kind: "noop", target: same.id };
3614
+ if (neighbours.length === 0) return { kind: "add" };
3615
+ if (mode === "keyword") return { kind: "ambiguous" };
3616
+ const best = neighbours[0];
3617
+ const score = best.score ?? 0;
3618
+ if (score >= cfg.dupThreshold && titlesMatch(candidate.title, best.title)) return { kind: "noop", target: best.id };
3619
+ if (score < cfg.novelThreshold) return { kind: "add" };
3620
+ return { kind: "ambiguous" };
3621
+ }
3622
+ async function gateCandidates(deps, ctx, candidates, cfg) {
3623
+ const { entityId, trigger } = ctx;
3624
+ const seen = /* @__PURE__ */ new Set();
3625
+ const unique = [];
3626
+ const inBatchDuplicates = [];
3627
+ for (const c of candidates) {
3628
+ const key = normalizeFactText(c.fact.title, c.fact.body);
3629
+ if (seen.has(key)) inBatchDuplicates.push(c);
3630
+ else {
3631
+ seen.add(key);
3632
+ unique.push(c);
3633
+ }
3634
+ }
3635
+ const nonLive = await deps.entryRepo.findNonLiveIdsByEntityIds([entityId], "current", Date.now());
3636
+ const canEmbed = typeof deps.options.llmProvider.embed === "function";
3637
+ const stored = [];
3638
+ if (canEmbed && unique.length > 0) {
3639
+ for (const row of await deps.entryRepo.findWithEmbeddingsByEntityIds([entityId])) {
3640
+ if (nonLive.has(row.id)) continue;
3641
+ const vec = parseEmbedding(row.embedding_blob, row.embedding);
3642
+ if (vec) stored.push({ id: row.id, vec });
3643
+ }
3644
+ }
3645
+ const gated = [];
3646
+ for (const c of unique) {
3647
+ let ranked = null;
3648
+ let vector = null;
3649
+ let mode = "keyword";
3650
+ if (canEmbed) {
3651
+ const r = await deps.embeddingService.embedTextForFact(
3652
+ { id: "candidate", entity_id: entityId, title: c.fact.title, body: c.fact.body, tags: c.fact.tags },
3653
+ void 0,
3654
+ { markFailures: false }
3655
+ );
3656
+ if (r.ok) {
3657
+ vector = r.vector;
3658
+ if (stored.length > 0 && stored.every((s) => s.vec.length === r.vector.length)) {
3659
+ ranked = stored.map((s) => ({ id: s.id, score: cosineSimilarity(r.vector, s.vec) })).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id)).slice(0, cfg.k);
3660
+ mode = "vector";
3661
+ }
3662
+ }
3663
+ }
3664
+ if (!ranked) {
3665
+ ranked = deps.searchService.searchKeyword(`${c.fact.title} ${c.fact.body}`, [entityId], cfg.k + nonLive.size).filter((h) => !nonLive.has(h.id)).slice(0, cfg.k).map((h) => ({ id: h.id, score: null }));
3666
+ }
3667
+ const facts = ranked.length > 0 ? await deps.entryRepo.findByIds(ranked.map((r) => r.id), [entityId]) : [];
3668
+ const byId = new Map(facts.map((f) => [f.id, f]));
3669
+ const neighbours = ranked.filter((r) => byId.has(r.id)).map((r, i) => {
3670
+ const f = byId.get(r.id);
3671
+ return { id: f.id, ref: `n${i + 1}`, title: f.title, body: f.body, source_type: f.source_type, score: r.score };
3672
+ });
3673
+ gated.push({ candidate: c, neighbours, decision: classifyCandidate(c.fact, neighbours, cfg, mode), vector });
3674
+ }
3675
+ const count = (k) => gated.filter((g) => g.decision.kind === k).length;
3676
+ const gateDiagnostic = {
3677
+ code: "librarian_gate",
3678
+ operation: "librarian",
3679
+ trigger,
3680
+ entityId,
3681
+ detail: { gateNoop: count("noop"), gateAdd: count("add"), gateAmbiguous: count("ambiguous") }
3682
+ };
3683
+ return { gated, inBatchDuplicates, gateDiagnostic };
3684
+ }
3685
+
3686
+ // src/services/librarian/ops/resolve.ts
3687
+ var OPS_RESOLVE_MAX_PROMPT_CHARS = 4e4;
3688
+ function validateOp(raw, aliases) {
3689
+ if (!raw || typeof raw !== "object") return null;
3690
+ const r = raw;
3691
+ if (r.op === "ADD") return { op: "ADD" };
3692
+ if (r.op !== "UPDATE" && r.op !== "SUPERSEDE" && r.op !== "NOOP") return null;
3693
+ const targetId = typeof r.target === "string" ? aliases.get(r.target) : void 0;
3694
+ if (!targetId) return null;
3695
+ if (r.op === "NOOP") return { op: "NOOP", targetId };
3696
+ if (r.op === "SUPERSEDE") {
3697
+ const validFrom = parseValidFrom(r.valid_from);
3698
+ return validFrom === void 0 ? { op: "SUPERSEDE", targetId } : { op: "SUPERSEDE", targetId, validFrom };
3699
+ }
3700
+ const update = { op: "UPDATE", targetId };
3701
+ if (typeof r.title === "string" && r.title.trim() !== "") update.title = clip(r.title, 80);
3702
+ if (typeof r.body === "string" && r.body.trim() !== "") update.body = clip(r.body, 800);
3703
+ return update;
3704
+ }
3705
+ var aliasMapOf = (g) => new Map(g.neighbours.map((n) => [n.ref, n.id]));
3706
+ async function runResolve(deps, ctx, ambiguous) {
3707
+ const out = { ops: /* @__PURE__ */ new Map(), rejected: [], failed: [] };
3708
+ if (ambiguous.length === 0) return out;
3709
+ const { entityId, trigger } = ctx;
3710
+ const outcome = await runBatched({
3711
+ items: ambiguous,
3712
+ buildPrompt: (batch) => deps.promptService.buildOpsResolvePrompt(
3713
+ batch.map((g, i) => ({
3714
+ item: i,
3715
+ candidate: { title: g.candidate.fact.title, body: g.candidate.fact.body },
3716
+ existing: g.neighbours.map((n) => ({
3717
+ ref: n.ref,
3718
+ title: n.title,
3719
+ body: safeSlice(n.body, 0, OPS_NEIGHBOUR_BODY_CHARS)
3720
+ }))
3721
+ }))
3722
+ ),
3723
+ call: (prompts) => callLlm(deps.options, { operation: "librarian", entityId, trigger, meter: ctx.meter }, prompts),
3724
+ parse: (text, batch) => {
3725
+ const parsed = parseJsonResponse(text);
3726
+ if (!Array.isArray(parsed.ops)) throw new Error("resolve: ops missing");
3727
+ return { batch, ops: parsed.ops };
3728
+ },
3729
+ maxPromptChars: OPS_RESOLVE_MAX_PROMPT_CHARS,
3730
+ maxOutputTokens: deps.options.llmProvider.maxOutputTokens
3731
+ });
3732
+ if (outcome.budgetStop) return { ...out, budgetStop: outcome.budgetStop };
3733
+ for (const { batch, ops } of outcome.results) {
3734
+ const byItem = /* @__PURE__ */ new Map();
3735
+ for (const raw of ops) {
3736
+ const item = raw?.item;
3737
+ if (typeof item === "number" && Number.isInteger(item) && item >= 0 && item < batch.length && !byItem.has(item)) {
3738
+ byItem.set(item, raw);
3739
+ }
3740
+ }
3741
+ batch.forEach((g, i) => {
3742
+ const op = validateOp(byItem.get(i), aliasMapOf(g));
3743
+ if (op) out.ops.set(g, op);
3744
+ else out.rejected.push(g);
3745
+ });
3746
+ }
3747
+ if (outcome.skipped.some((s) => s.reason === "call_error")) {
3748
+ throw new Error("resolve: transient provider error; retry the pass");
3749
+ }
3750
+ for (const s of outcome.skipped) out.failed.push(s.item);
3751
+ return out;
3752
+ }
3753
+
3754
+ // src/utils/temporal.ts
3755
+ function liveAtSql(mode, alias = "") {
3756
+ const from = mode === "asOf" ? `COALESCE(${alias}valid_from, ${alias}created_at) <= ?` : `(${alias}valid_from IS NULL OR ${alias}valid_from <= ?)`;
3757
+ return `(${from} AND (${alias}valid_to IS NULL OR ${alias}valid_to > ?))`;
3758
+ }
3759
+ function isLiveAt(f, mode, t) {
3760
+ if (f.deleted_at != null) return false;
3761
+ const from = mode === "asOf" ? f.valid_from ?? f.created_at : f.valid_from;
3762
+ if (from != null && from > t) return false;
3763
+ return f.valid_to == null || f.valid_to > t;
3764
+ }
3765
+ function assertEpochMs(name, v) {
3766
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0) {
3767
+ throw new TypeError(`${name} must be a finite epoch-ms number >= 0`);
3768
+ }
3769
+ return Math.trunc(v);
3770
+ }
3771
+
3772
+ // src/services/TemporalService.ts
3773
+ var HISTORY_MAX_DEPTH = 100;
3774
+ var TemporalService = class {
3775
+ constructor(db, options, entryRepo, searchService, embeddingService) {
3776
+ this.db = db;
3777
+ this.options = options;
3778
+ this.entryRepo = entryRepo;
3779
+ this.searchService = searchService;
3780
+ this.embeddingService = embeddingService;
3781
+ }
3782
+ /**
3783
+ * Link `oldId` → `newId` inside the caller's transaction. `newId` must
3784
+ * already exist. Throws WikiSupersedeError on any rule violation; the
3785
+ * caller's transaction then rolls back.
3786
+ */
3787
+ async supersedeInTx(tx, entityId, oldId, newId, t, now) {
3788
+ const old = await this.entryRepo.findTemporalRow(oldId, tx);
3789
+ if (!old || old.deleted_at != null) throw new WikiSupersedeError("not_found");
3790
+ if (old.entity_id !== entityId) throw new WikiSupersedeError("cross_entity");
3791
+ if (old.superseded_by != null) throw new WikiSupersedeError("already_superseded");
3792
+ if (old.source_type === "immutable_document") throw new WikiSupersedeError("immutable_target");
3793
+ if (newId === oldId) throw new WikiSupersedeError("cycle");
3794
+ const repl = await this.entryRepo.findTemporalRow(newId, tx);
3795
+ if (!repl || repl.deleted_at != null) throw new WikiSupersedeError("not_found");
3796
+ if (repl.entity_id !== entityId) throw new WikiSupersedeError("cross_entity");
3797
+ if (repl.superseded_by != null) throw new WikiSupersedeError("already_superseded");
3798
+ const { predecessors } = await this.entryRepo.findSupersessionChainIds(entityId, oldId, HISTORY_MAX_DEPTH, tx);
3799
+ if (predecessors.includes(newId)) throw new WikiSupersedeError("cycle");
3800
+ await this.entryRepo.setTemporal(oldId, entityId, { valid_to: t, superseded_by: newId, superseded_at: now }, tx);
3801
+ if (repl.valid_from == null) {
3802
+ await this.entryRepo.setTemporal(newId, entityId, { valid_from: t }, tx);
3803
+ }
3804
+ }
3805
+ async supersede(entityId, oldId, replacement, options) {
3806
+ const now = Date.now();
3807
+ const t = options?.validFrom === void 0 ? now : assertEpochMs("options.validFrom", options.validFrom);
3808
+ let created = null;
3809
+ if (typeof replacement !== "string") {
3810
+ if (typeof replacement?.title !== "string" || replacement.title.trim() === "" || typeof replacement.body !== "string") {
3811
+ throw new TypeError("supersede(): replacement must be a fact id or { title, body }");
3812
+ }
3813
+ created = {
3814
+ id: generateId("fact_"),
3815
+ entity_id: entityId,
3816
+ title: replacement.title,
3817
+ body: replacement.body,
3818
+ tags: Array.isArray(replacement.tags) ? replacement.tags : [],
3819
+ confidence: replacement.confidence ?? "certain",
3820
+ source_type: replacement.source_type ?? "user_stated",
3821
+ source_hash: null,
3822
+ source_ref: null,
3823
+ created_at: now,
3824
+ updated_at: now,
3825
+ last_accessed_at: null,
3826
+ access_count: 0,
3827
+ deleted_at: null
3828
+ };
3829
+ }
3830
+ const newId = typeof replacement === "string" ? replacement : created.id;
3831
+ const newValidFrom = typeof replacement !== "string" && replacement.valid_from !== void 0 ? assertEpochMs("replacement.valid_from", replacement.valid_from) : void 0;
3832
+ await this.db.withTransactionAsync(async (tx) => {
3833
+ if (created) {
3834
+ await this.entryRepo.upsert(created, tx);
3835
+ if (newValidFrom !== void 0) {
3836
+ await this.entryRepo.setTemporal(created.id, entityId, { valid_from: newValidFrom }, tx);
3837
+ }
3838
+ }
3839
+ await this.supersedeInTx(tx, entityId, oldId, newId, t, now);
3840
+ });
3841
+ await this.searchService.syncEntries(entityId, [oldId, newId]);
3842
+ if (created) {
3843
+ await this.embeddingService.embedFact(
3844
+ { id: created.id, entity_id: entityId, title: created.title, body: created.body, tags: created.tags },
3845
+ { operation: "supersede", trigger: "call" }
3846
+ );
3847
+ }
3848
+ this.searchService.evictCache(entityId);
3849
+ return { newId };
3850
+ }
3851
+ async history(entityId, factId) {
3852
+ const start = await this.entryRepo.findTemporalRow(factId);
3853
+ if (!start || start.entity_id !== entityId || start.deleted_at != null) return [];
3854
+ const { predecessors, successors } = await this.entryRepo.findSupersessionChainIds(entityId, factId, HISTORY_MAX_DEPTH);
3855
+ const facts = await this.entryRepo.findByIds([...predecessors, factId, ...successors], [entityId]);
3856
+ return facts.sort((a, b) => (a.valid_from ?? a.created_at) - (b.valid_from ?? b.created_at) || a.created_at - b.created_at || a.id.localeCompare(b.id));
3857
+ }
3858
+ };
3859
+
3860
+ // src/services/librarian/ops/apply.ts
3861
+ async function applyOps(deps, ctx, input) {
3862
+ const { entityId, trigger } = ctx;
3863
+ const diagBase = { entityId, operation: "librarian", trigger };
3864
+ const grounding = deps.promptService.groundingFor("librarian");
3865
+ const temporal = new TemporalService(deps.db, deps.options, deps.entryRepo, deps.searchService, deps.embeddingService);
3866
+ const rejected = new Set(input.rejected);
3867
+ const failed = new Set(input.failed);
3868
+ const written = [];
3869
+ const updatedIds = [];
3870
+ const now = Date.now();
3871
+ await deps.db.withTransactionAsync(async (tx) => {
3872
+ let { mode, manifest } = await deps.ontologyService?.getEffectiveState(entityId, tx) ?? { mode: "off", manifest: { node_types: [], edge_types: [] } };
3873
+ if (mode === "emergent" && input.ontologyUpdates && deps.ontologyService) {
3874
+ manifest = await deps.ontologyService.mergeEmergentUpdates(entityId, input.ontologyUpdates, tx);
3875
+ }
3876
+ const titleIndex = /* @__PURE__ */ new Map();
3877
+ for (const f of await deps.entryRepo.findRecentByEntityId(entityId, 100, tx)) {
3878
+ titleIndex.set(normalizeTitleKey(f.title), { id: f.id, okf_type: f.okf_type ?? null });
3879
+ }
3880
+ const pendingEdges = [];
3881
+ const insert = async (g, draft, validFrom, overrides) => {
3882
+ const fact = g.candidate.fact;
3883
+ const title = overrides?.title ?? fact.title;
3884
+ const body = overrides?.body ?? fact.body;
3885
+ const id = generateId("fact_");
3886
+ const drops = [];
3887
+ const normalized = deps.ontologyService?.validateAndNormalizeFact(fact, manifest, { strict: false, drops }) ?? { okf_type: null, edges: [] };
3888
+ for (const d of drops) input.diagnostics.push(edgeDropDiagnostic(d, { ...diagBase, factId: id }));
3889
+ const g2 = grounding ? groundingOutcome(checkGrounding(fact.evidence, input.groundingCorpus ?? [], grounding), now) : null;
3890
+ if (g2?.diagnostic) {
3891
+ input.diagnostics.push({
3892
+ ...diagBase,
3893
+ code: g2.diagnostic.code,
3894
+ detail: { factId: id, itemIndex: g.candidate.index, reason: g2.diagnostic.reason }
3895
+ });
3896
+ }
3897
+ const row = {
3898
+ id,
3899
+ entity_id: entityId,
3900
+ title,
3901
+ body,
3902
+ tags: fact.tags,
3903
+ confidence: fact.confidence,
3904
+ source_type: "librarian_inferred",
3905
+ source_hash: null,
3906
+ source_ref: null,
3907
+ created_at: now,
3908
+ updated_at: now,
3909
+ last_accessed_at: null,
3910
+ access_count: 0,
3911
+ deleted_at: null,
3912
+ okf_type: normalized.okf_type,
3913
+ ...g2?.trust,
3914
+ ...draft ? { lifecycle_status: "draft" } : {}
3915
+ };
3916
+ await deps.entryRepo.upsert(row, tx);
3917
+ await deps.entryRepo.setTemporal(id, entityId, { valid_from: validFrom }, tx);
3918
+ titleIndex.set(normalizeTitleKey(title), { id, okf_type: normalized.okf_type });
3919
+ if (normalized.edges.length > 0) pendingEdges.push({ sourceId: id, sourceType: normalized.okf_type, edges: normalized.edges });
3920
+ const textChanged = title !== fact.title || body !== fact.body;
3921
+ written.push({ id, vector: textChanged ? null : g.vector, fact: { id, entity_id: entityId, title, body, tags: fact.tags } });
3922
+ return id;
3923
+ };
3924
+ for (const g of input.gated) {
3925
+ const idx = g.candidate.index;
3926
+ const vf = g.candidate.validFrom;
3927
+ if (g.decision.kind === "noop") continue;
3928
+ if (g.decision.kind === "add") {
3929
+ await insert(g, false, vf);
3930
+ continue;
3931
+ }
3932
+ if (failed.has(g)) {
3933
+ const id = await insert(g, true, vf);
3934
+ input.diagnostics.push({ ...diagBase, code: "resolve_failed", detail: { factId: id, itemIndex: idx } });
3935
+ continue;
3936
+ }
3937
+ const op = input.ops.get(g);
3938
+ if (rejected.has(g) || !op) {
3939
+ await insert(g, false, vf);
3940
+ input.diagnostics.push({ ...diagBase, code: "librarian_op_rejected", detail: { itemIndex: idx, reason: "invalid_op" } });
3941
+ continue;
3942
+ }
3943
+ if (op.op === "NOOP") continue;
3944
+ if (op.op === "ADD") {
3945
+ await insert(g, false, vf);
3946
+ continue;
3947
+ }
3948
+ const target = g.neighbours.find((n) => n.id === op.targetId);
3949
+ if (op.op === "UPDATE" && target.source_type === "librarian_inferred") {
3950
+ const [existing] = await deps.entryRepo.findByIds([op.targetId], [entityId], tx);
3951
+ if (existing) {
3952
+ const newTitle = op.title ?? g.candidate.fact.title;
3953
+ await deps.entryRepo.upsert({
3954
+ ...existing,
3955
+ title: newTitle,
3956
+ body: op.body ?? g.candidate.fact.body,
3957
+ updated_at: now
3958
+ }, tx);
3959
+ titleIndex.delete(normalizeTitleKey(existing.title));
3960
+ titleIndex.set(normalizeTitleKey(newTitle), { id: existing.id, okf_type: existing.okf_type ?? null });
3961
+ updatedIds.push(existing.id);
3962
+ continue;
3963
+ }
3964
+ }
3965
+ const supersedeFrom = op.op === "SUPERSEDE" && op.validFrom !== void 0 ? op.validFrom : vf;
3966
+ const overrides = op.op === "UPDATE" ? { title: op.title, body: op.body } : void 0;
3967
+ if (target.source_type === "immutable_document") {
3968
+ const id = await insert(g, true, supersedeFrom, overrides);
3969
+ input.diagnostics.push({ ...diagBase, code: "contradicts_document", detail: { factId: id, itemIndex: idx } });
3970
+ continue;
3971
+ }
3972
+ const newId = await insert(g, false, supersedeFrom, overrides);
3973
+ try {
3974
+ await temporal.supersedeInTx(tx, entityId, op.targetId, newId, supersedeFrom, now);
3975
+ } catch (err) {
3976
+ if (!(err instanceof WikiSupersedeError)) throw err;
3977
+ input.diagnostics.push({
3978
+ ...diagBase,
3979
+ code: "librarian_op_rejected",
3980
+ detail: { factId: newId, itemIndex: idx, reason: err.reason }
3981
+ });
3982
+ }
3983
+ }
3984
+ for (const item of pendingEdges) {
3985
+ const resolveDrops = [];
3986
+ await deps.ontologyService?.resolveAndPersistEdges(
3987
+ entityId,
3988
+ item.sourceId,
3989
+ item.sourceType,
3990
+ item.edges,
3991
+ manifest,
3992
+ titleIndex,
3993
+ tx,
3994
+ now,
3995
+ resolveDrops
3996
+ );
3997
+ for (const d of resolveDrops) input.diagnostics.push(edgeDropDiagnostic(d, diagBase));
3998
+ }
3999
+ for (const task of input.tasks) {
4000
+ const taskObj = {
4001
+ id: generateId("task_"),
4002
+ entity_id: entityId,
4003
+ description: task.description,
4004
+ status: "pending",
4005
+ priority: task.priority,
4006
+ created_at: now,
4007
+ updated_at: now,
4008
+ resolved_at: null,
4009
+ deleted_at: null
4010
+ };
4011
+ await deps.taskRepo.upsert(taskObj, tx);
4012
+ }
4013
+ });
4014
+ return { written, updatedIds };
4015
+ }
4016
+
4017
+ // src/services/librarian/ops/index.ts
4018
+ var runOpsLibrarianPass = async (deps, ctx) => {
4019
+ const { entityId, trigger } = ctx;
4020
+ const cfg = resolveGateConfig(deps.options.config?.librarian?.gate);
4021
+ await deps.seedWatermark(entityId);
4022
+ const wm = await deps.getWatermark(entityId);
4023
+ const sel = await selectBatch(deps, entityId, wm, ctx.meter);
4024
+ if ("budgetStop" in sel) return { processedThrough: null, factsWritten: 0, budgetStop: sel.budgetStop };
4025
+ if (sel.batch.length === 0) return { processedThrough: null, factsWritten: 0 };
4026
+ let ex;
4027
+ try {
4028
+ ex = await runExtract(deps, ctx, sel.batch);
4029
+ } catch (err) {
4030
+ if (err instanceof WikiBudgetExhausted) {
4031
+ return { processedThrough: null, factsWritten: 0, budgetStop: { requiredEstimate: err.requiredEstimate } };
4032
+ }
4033
+ throw err;
4034
+ }
4035
+ const diagnostics = new DiagnosticBuffer();
4036
+ for (const d of ex.diagnostics) diagnostics.push(d);
4037
+ const { gated, inBatchDuplicates, gateDiagnostic } = await gateCandidates(deps, ctx, ex.candidates, cfg);
4038
+ for (const dup of inBatchDuplicates) {
4039
+ diagnostics.push({
4040
+ code: "fact_deduplicated",
4041
+ operation: "librarian",
4042
+ trigger,
4043
+ entityId,
4044
+ detail: { itemIndex: dup.index, reason: "in_batch" }
4045
+ });
4046
+ }
4047
+ diagnostics.push(gateDiagnostic);
4048
+ const res = await runResolve(deps, ctx, gated.filter((g) => g.decision.kind === "ambiguous"));
4049
+ if (res.budgetStop) {
4050
+ diagnostics.discard();
4051
+ return { processedThrough: null, factsWritten: 0, budgetStop: res.budgetStop };
4052
+ }
4053
+ const { written, updatedIds } = await applyOps(deps, ctx, {
4054
+ gated,
4055
+ ops: res.ops,
4056
+ rejected: res.rejected,
4057
+ failed: res.failed,
4058
+ tasks: ex.tasks,
4059
+ ontologyUpdates: ex.ontologyUpdates,
4060
+ groundingCorpus: ex.groundingCorpus,
4061
+ diagnostics
4062
+ });
4063
+ diagnostics.flush(deps.options);
4064
+ await deps.searchService.syncEntries(entityId, [...written.map((w) => w.id), ...updatedIds]);
4065
+ const embedCtx = { operation: "librarian", trigger };
4066
+ for (const w of written) {
4067
+ if (w.vector) await deps.embeddingService.storeFactVector(w.fact, w.vector, embedCtx);
4068
+ else await deps.embeddingService.embedFact(w.fact, embedCtx);
4069
+ }
4070
+ if (updatedIds.length > 0) {
4071
+ for (const f of await deps.entryRepo.findByIds(updatedIds, [entityId])) {
4072
+ await deps.embeddingService.embedFact(
4073
+ { id: f.id, entity_id: f.entity_id, title: f.title, body: f.body, tags: f.tags },
4074
+ embedCtx
4075
+ );
4076
+ }
4077
+ }
4078
+ deps.searchService.evictCache(entityId);
4079
+ const last = sel.batch[sel.batch.length - 1].event;
4080
+ return {
4081
+ processedThrough: { at: last.created_at, id: last.id },
4082
+ factsWritten: written.length + updatedIds.length
4083
+ };
4084
+ };
4085
+
4086
+ // src/services/librarian/index.ts
4087
+ var STRATEGIES = /* @__PURE__ */ new Map([
4088
+ ["legacy", runLegacyLibrarianPass],
4089
+ ["ops", runOpsLibrarianPass]
4090
+ ]);
4091
+ async function runLibrarianStrategy(deps, ctx) {
4092
+ const strategy = deps.options.config?.librarian?.strategy ?? "legacy";
4093
+ const fn = STRATEGIES.get(strategy);
4094
+ if (!fn) {
4095
+ throw new Error(`Unknown librarian strategy: ${strategy}`);
4096
+ }
4097
+ return fn(deps, ctx);
4098
+ }
4099
+
4100
+ // src/services/MaintenanceService.ts
3163
4101
  var ONTOLOGY_BACKFILL_BATCH_SIZE = 25;
3164
4102
  var ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS = 4e4;
3165
4103
  var ONTOLOGY_BACKFILL_RECHECK_MS = 7 * 24 * 60 * 60 * 1e3;
@@ -3232,11 +4170,23 @@ var MaintenanceService = class {
3232
4170
  const vacuum = options?.vacuum ?? false;
3233
4171
  this._validatePruneDuration(retainSoftDeletedFor, "retainSoftDeletedFor");
3234
4172
  this._validatePruneDuration(retainEventsFor, "retainEventsFor");
4173
+ const pruneSupersededAfter = this.options.config?.pruneSupersededAfter ?? null;
4174
+ this._validatePruneDuration(pruneSupersededAfter, "pruneSupersededAfter");
3235
4175
  const now = Date.now();
3236
4176
  let syncedIds = [];
3237
4177
  let deletedEntries = 0;
3238
4178
  let deletedTasks = 0;
3239
4179
  let deletedEvents = 0;
4180
+ if (pruneSupersededAfter !== null) {
4181
+ const expiredCutoff = now - pruneSupersededAfter * 864e5;
4182
+ const expiredIds = await this.entryRepo.findExpiredIds(entityId, expiredCutoff);
4183
+ if (expiredIds.length > 0) {
4184
+ await this.db.withTransactionAsync(async (tx) => {
4185
+ await this.entryRepo.softDeleteByIds(expiredIds, entityId, tx);
4186
+ });
4187
+ syncedIds = [...expiredIds];
4188
+ }
4189
+ }
3240
4190
  if (retainSoftDeletedFor !== null) {
3241
4191
  const cutoff = now - retainSoftDeletedFor * 864e5;
3242
4192
  const entriesToDelete = await this.entryRepo.getPrunableMetadata(entityId, cutoff);
@@ -3252,7 +4202,7 @@ var MaintenanceService = class {
3252
4202
  }
3253
4203
  }
3254
4204
  const succeededIds = succeeded.map((r) => r.id);
3255
- syncedIds = succeededIds;
4205
+ syncedIds = [...syncedIds, ...succeededIds];
3256
4206
  await this.db.withTransactionAsync(async (tx) => {
3257
4207
  if (succeededIds.length > 0) {
3258
4208
  deletedEntries = await this.entryRepo.bulkDeletePruned(entityId, cutoff, succeededIds, tx);
@@ -3288,8 +4238,18 @@ var MaintenanceService = class {
3288
4238
  }
3289
4239
  if (retainEventsFor !== null) {
3290
4240
  const cutoff = now - retainEventsFor * 864e5;
3291
- const eventResult = await this.eventRepo.prune(entityId, cutoff);
3292
- deletedEvents = eventResult.changes;
4241
+ const guarded = this.options.config?.maintenance === "deferred" || this.options.config?.librarian?.strategy === "ops";
4242
+ if (guarded) {
4243
+ const wm = await this.metadataRepo.getLibrarianWatermark(entityId, this.db);
4244
+ deletedEvents = (await this.eventRepo.pruneThrough(entityId, cutoff, wm)).changes;
4245
+ const held = await this.eventRepo.countHeldBack(entityId, cutoff, wm);
4246
+ if (held > 0) {
4247
+ emitDiagnostic(this.options, { code: "event_retention_held", operation: "prune", trigger: "call", entityId, detail: { count: held } });
4248
+ }
4249
+ } else {
4250
+ const eventResult = await this.eventRepo.prune(entityId, cutoff);
4251
+ deletedEvents = eventResult.changes;
4252
+ }
3293
4253
  }
3294
4254
  if (vacuum) {
3295
4255
  await this.metadataRepo.vacuum();
@@ -3519,155 +4479,72 @@ var MaintenanceService = class {
3519
4479
  }
3520
4480
  /** Core librarian pass (locks handled by {@link runLibrarian}). Package-internal orchestration hook. */
3521
4481
  async doRunLibrarian(entityId, promptOverride, trigger = "call") {
3522
- const events = await this.eventRepo.getRecent(entityId, 50);
3523
- const currentFactsRows = await this.entryRepo.findRecentByEntityId(entityId, 100);
3524
- const currentFacts = currentFactsRows.map((f) => {
3525
- const { embedding: _embedding, embedding_blob: _blob, ...rest } = f;
3526
- return {
3527
- ...rest,
3528
- tags: typeof rest.tags === "string" ? JSON.parse(rest.tags) : rest.tags
3529
- };
3530
- });
3531
- const ontologyContext = await this.ontologyService?.buildPromptContext(entityId) ?? null;
3532
- const promptEvents = events.reverse();
3533
- const librarianGrounding = this.promptService.groundingFor("librarian");
3534
- const { systemPrompt, userPrompt, groundingCorpus: librarianCorpus = [] } = this.promptService.buildLibrarianPrompt(
3535
- promptEvents,
3536
- currentFacts,
3537
- promptOverride,
3538
- ontologyContext
3539
- );
3540
- const responseText = await this.options.llmProvider.generateText({ systemPrompt, userPrompt });
3541
- const result = parseJsonResponse(responseText);
3542
- const facts = Array.isArray(result.facts) ? result.facts : [];
3543
- const tasks = Array.isArray(result.tasks) ? result.tasks : [];
3544
- const ontologyUpdates = result.ontology_updates;
3545
- const diagBuffer = new DiagnosticBuffer();
3546
- const diagBase = { entityId, operation: "librarian", trigger };
3547
- const validFacts = [];
3548
- const validFactItemIndexes = [];
3549
- facts.forEach((raw, itemIndex) => {
3550
- const valid = validateFact(raw);
3551
- if (valid) {
3552
- validFacts.push(valid);
3553
- validFactItemIndexes.push(itemIndex);
3554
- } else {
3555
- diagBuffer.push({ ...diagBase, code: "fact_rejected", detail: { itemIndex, reason: factRejectionReason(raw) } });
3556
- }
3557
- });
3558
- const validTasks = [];
3559
- tasks.forEach((raw, itemIndex) => {
3560
- const valid = validateTask(raw);
3561
- if (valid) validTasks.push(valid);
3562
- else diagBuffer.push({ ...diagBase, code: "task_rejected", detail: { itemIndex, reason: taskRejectionReason(raw) } });
3563
- });
3564
- const now = Date.now();
3565
- const insertedFacts = [];
3566
- await this.db.withTransactionAsync(async (tx) => {
3567
- let { mode, manifest } = await this.ontologyService?.getEffectiveState(entityId, tx) ?? { mode: "off", manifest: { node_types: [], edge_types: [] } };
3568
- if (mode === "emergent" && ontologyUpdates && this.ontologyService) {
3569
- manifest = await this.ontologyService.mergeEmergentUpdates(entityId, ontologyUpdates, tx);
3570
- }
3571
- const titleIndex = /* @__PURE__ */ new Map();
3572
- for (const existing of currentFactsRows) {
3573
- titleIndex.set(normalizeTitleKey(existing.title), {
3574
- id: existing.id,
3575
- okf_type: existing.okf_type ?? null
3576
- });
3577
- }
3578
- const factsForDedupe = await this.entryRepo.findRecentByEntityId(entityId, 100, tx);
3579
- const pendingEdges = [];
3580
- for (const [k, fact] of validFacts.entries()) {
3581
- const newTokens = titleTokens(fact.title);
3582
- let skip = false;
3583
- if (newTokens.size >= MIN_TOKENS_TO_QUALIFY) {
3584
- for (const existing of factsForDedupe) {
3585
- if (existing.source_type !== "librarian_inferred") continue;
3586
- const existingTokens = titleTokens(existing.title);
3587
- if (existingTokens.size >= MIN_TOKENS_TO_QUALIFY) {
3588
- if (jaccardScore(newTokens, existingTokens) >= FUZZY_THRESHOLD) {
3589
- skip = true;
3590
- break;
3591
- }
3592
- }
3593
- }
3594
- }
3595
- if (skip) {
3596
- diagBuffer.push({ ...diagBase, code: "fact_deduplicated", detail: { itemIndex: validFactItemIndexes[k], reason: "fuzzy_title" } });
3597
- continue;
3598
- }
3599
- const ontologyFact = fact;
3600
- const id = generateId("fact_");
3601
- const validationDrops = [];
3602
- const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false, drops: validationDrops }) ?? { okf_type: null, edges: [] };
3603
- for (const drop of validationDrops) diagBuffer.push(edgeDropDiagnostic(drop, { ...diagBase, factId: id }));
3604
- const grounding = librarianGrounding ? groundingOutcome(checkGrounding(fact.evidence, librarianCorpus, librarianGrounding), now) : null;
3605
- if (grounding?.diagnostic) {
3606
- diagBuffer.push({ ...diagBase, code: grounding.diagnostic.code, detail: { factId: id, itemIndex: validFactItemIndexes[k], reason: grounding.diagnostic.reason } });
3607
- }
3608
- const factObj = {
3609
- id,
3610
- entity_id: entityId,
3611
- title: fact.title,
3612
- body: fact.body,
3613
- tags: fact.tags,
3614
- confidence: fact.confidence,
3615
- source_type: "librarian_inferred",
3616
- source_hash: null,
3617
- source_ref: null,
3618
- created_at: now,
3619
- updated_at: now,
3620
- last_accessed_at: null,
3621
- access_count: 0,
3622
- deleted_at: null,
3623
- okf_type: normalized.okf_type,
3624
- ...grounding?.trust
3625
- };
3626
- await this.entryRepo.upsert(factObj, tx);
3627
- insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
3628
- factsForDedupe.push(factObj);
3629
- titleIndex.set(normalizeTitleKey(fact.title), { id, okf_type: normalized.okf_type });
3630
- if (normalized.edges.length > 0) {
3631
- pendingEdges.push({ sourceId: id, sourceType: normalized.okf_type, edges: normalized.edges });
3632
- }
3633
- }
3634
- for (const item of pendingEdges) {
3635
- const resolveDrops = [];
3636
- await this.ontologyService?.resolveAndPersistEdges(
3637
- entityId,
3638
- item.sourceId,
3639
- item.sourceType,
3640
- item.edges ?? [],
3641
- manifest,
3642
- titleIndex,
3643
- tx,
3644
- now,
3645
- resolveDrops
3646
- );
3647
- for (const drop of resolveDrops) diagBuffer.push(edgeDropDiagnostic(drop, diagBase));
3648
- }
3649
- for (const task of validTasks) {
3650
- const id = generateId("task_");
3651
- const taskObj = {
3652
- id,
3653
- entity_id: entityId,
3654
- description: task.description,
3655
- status: "pending",
3656
- priority: task.priority,
3657
- created_at: now,
3658
- updated_at: now,
3659
- resolved_at: null,
3660
- deleted_at: null
3661
- };
3662
- await this.taskRepo.upsert(taskObj, tx);
3663
- }
3664
- });
3665
- diagBuffer.flush(this.options);
3666
- await this.searchService.syncEntries(entityId, insertedFacts.map((f) => f.id));
3667
- for (const fact of insertedFacts) {
3668
- await this.embeddingService.embedFact(fact, { operation: "librarian", trigger });
4482
+ await this.runLibrarianPass({ entityId, trigger, ...promptOverride !== void 0 ? { promptOverride } : {} });
4483
+ }
4484
+ /**
4485
+ * Seam between the lock wrapper and the strategy dispatcher (spec 2026-09-29
4486
+ * §5.1). Builds the strategy deps from `this` and dispatches via
4487
+ * `runLibrarianStrategy` so PR-B can plug in `ops/` without touching this
4488
+ * class. Returns the strategy's `LibrarianResult` so deferred/budgeted
4489
+ * schedulers (PR-C) can advance watermarks; the existing void-returning
4490
+ * `doRunLibrarian` discards it.
4491
+ *
4492
+ * Spec §10.3: one place advances the watermark, for every strategy, only
4493
+ * after the pass resolved (its transaction committed) and never backwards.
4494
+ * A budget-stopped pass wrote nothing and must not move the watermark.
4495
+ */
4496
+ async runLibrarianPass(ctx) {
4497
+ const result = await runLibrarianStrategy(this.librarianDeps(), ctx);
4498
+ if (result.processedThrough && !result.budgetStop) {
4499
+ await this.advanceLibrarianWatermark(ctx.entityId, result.processedThrough);
3669
4500
  }
3670
- this.searchService.evictCache(entityId);
4501
+ return result;
4502
+ }
4503
+ /**
4504
+ * Strategy deps shared between legacy and ops librarians (spec §5.1). The
4505
+ * ops strategy (PR-B Task 8) reads/anchors the watermark through these two
4506
+ * hooks so the dispatcher itself never reaches into the metadata table —
4507
+ * the watermark invariant (one place advances, never backwards) lives here.
4508
+ */
4509
+ librarianDeps() {
4510
+ return {
4511
+ db: this.db,
4512
+ options: this.options,
4513
+ entryRepo: this.entryRepo,
4514
+ taskRepo: this.taskRepo,
4515
+ eventRepo: this.eventRepo,
4516
+ searchService: this.searchService,
4517
+ embeddingService: this.embeddingService,
4518
+ promptService: this.promptService,
4519
+ ontologyService: this.ontologyService,
4520
+ seedWatermark: (entityId) => this.seedLibrarianWatermark(entityId),
4521
+ getWatermark: (entityId) => this.metadataRepo.getLibrarianWatermark(entityId, this.db)
4522
+ };
4523
+ }
4524
+ /**
4525
+ * Persist `next` as the entity's librarian watermark when it is strictly
4526
+ * newer than the stored value, under the (created_at, id) ordering used by
4527
+ * the op-based librarian (spec §10.3). Never moves backwards.
4528
+ */
4529
+ async advanceLibrarianWatermark(entityId, next) {
4530
+ const current = await this.metadataRepo.getLibrarianWatermark(entityId, this.db);
4531
+ const newer = !current || next.at > current.at || next.at === current.at && next.id > current.id;
4532
+ if (newer) await this.metadataRepo.setLibrarianWatermark(entityId, next, this.db);
4533
+ }
4534
+ /**
4535
+ * Spec §10.3: first ops pass on a legacy DB starts after what the count
4536
+ * checkpoint already covered. No-op when a watermark already exists or the
4537
+ * memory checkpoint is 0; otherwise anchor the watermark to the event at
4538
+ * offset `min(memory_checkpoint, totalEvents) - 1`.
4539
+ */
4540
+ async seedLibrarianWatermark(entityId) {
4541
+ if (await this.metadataRepo.getLibrarianWatermark(entityId, this.db)) return;
4542
+ const cp = await this.metadataRepo.getCheckpoint(entityId, this.db);
4543
+ const memory = cp.memory ?? 0;
4544
+ if (memory <= 0) return;
4545
+ const total = await this.eventRepo.count(entityId);
4546
+ const ev = await this.eventRepo.getAtOffset(entityId, Math.min(memory, total) - 1);
4547
+ if (ev) await this.metadataRepo.setLibrarianWatermark(entityId, { at: ev.created_at, id: ev.id }, this.db);
3671
4548
  }
3672
4549
  /**
3673
4550
  * Core heal pass (locks handled by {@link runHeal}). Package-internal orchestration hook.
@@ -3773,7 +4650,7 @@ var MaintenanceService = class {
3773
4650
  if (groundingCorpus !== void 0) corpusByPrompt.set(prompts, groundingCorpus);
3774
4651
  return prompts;
3775
4652
  },
3776
- call: (prompts) => this.options.llmProvider.generateText(prompts),
4653
+ call: (prompts) => callLlm(this.options, { operation: "heal", entityId, trigger, meter: options?.meter }, prompts),
3777
4654
  parse: (responseText, batch, prompts) => {
3778
4655
  const result = parseJsonResponse(responseText);
3779
4656
  return {
@@ -3877,9 +4754,10 @@ var MaintenanceService = class {
3877
4754
  const callErrorIds = new Set(
3878
4755
  outcome.skipped.filter((s) => s.reason === "call_error").map((s) => s.item.id)
3879
4756
  );
4757
+ const unattemptedIds = new Set(outcome.unattempted.map((f) => f.id));
3880
4758
  await this.entryRepo.markHealChecked(
3881
4759
  [
3882
- ...healCandidates.map((f) => f.id).filter((id) => !callErrorIds.has(id)),
4760
+ ...healCandidates.map((f) => f.id).filter((id) => !callErrorIds.has(id) && !unattemptedIds.has(id)),
3883
4761
  ...insertedFacts.map((f) => f.id)
3884
4762
  ],
3885
4763
  entityId,
@@ -3931,7 +4809,8 @@ var MaintenanceService = class {
3931
4809
  skipped: outcome.skipped.map(({ item, reason }) => ({ id: item.id, reason })),
3932
4810
  degraded: healedDegraded,
3933
4811
  remaining: counts.eligible,
3934
- deferred: counts.deferred
4812
+ deferred: counts.deferred,
4813
+ ...outcome.budgetStop ? { budgetStop: outcome.budgetStop } : {}
3935
4814
  };
3936
4815
  }
3937
4816
  /** Core ontology backfill pass (locks handled by {@link runOntologyBackfill}). Package-internal orchestration hook. */
@@ -3975,7 +4854,7 @@ var MaintenanceService = class {
3975
4854
  const outcome = await runBatched({
3976
4855
  items: candidates,
3977
4856
  buildPrompt,
3978
- call: (prompts) => this.options.llmProvider.generateText(prompts),
4857
+ call: (prompts) => callLlm(this.options, { operation: "ontologyBackfill", entityId, trigger: "call" }, prompts),
3979
4858
  parse: (responseText, batch) => {
3980
4859
  const parsed = parseJsonResponse(responseText);
3981
4860
  return {
@@ -4506,6 +5385,12 @@ var ImportExportService = class {
4506
5385
  })()
4507
5386
  };
4508
5387
  await this.entryRepo.upsertForImport(factObj, tx);
5388
+ await this.entryRepo.setTemporal(fact.id, entityId, {
5389
+ valid_from: fact.valid_from ?? null,
5390
+ valid_to: fact.valid_to ?? null,
5391
+ superseded_by: fact.superseded_by ?? null,
5392
+ superseded_at: fact.superseded_at ?? null
5393
+ }, tx, { outbox: false });
4509
5394
  if (blobData != null) {
4510
5395
  factsWithPreservedBlob.set(fact.id, blobData);
4511
5396
  if (!fact.deleted_at) preservedBlobDims.add(blobData.byteLength / 4);
@@ -4584,7 +5469,8 @@ var ImportExportService = class {
4584
5469
  event_type: event.event_type,
4585
5470
  summary: event.summary,
4586
5471
  related_entry_id: event.related_entry_id ?? null,
4587
- created_at: event.created_at
5472
+ created_at: event.created_at,
5473
+ occurred_at: event.occurred_at ?? null
4588
5474
  },
4589
5475
  tx
4590
5476
  );
@@ -4831,6 +5717,21 @@ var EmbeddingService = class {
4831
5717
  }
4832
5718
  }
4833
5719
  async tryEmbedFact(fact, ctx) {
5720
+ const r = await this.embedTextForFact(fact, ctx);
5721
+ return r.ok ? this.storeFactVector(fact, r.vector, ctx) : r;
5722
+ }
5723
+ /**
5724
+ * Compute half: build the embed text, call embed(), validate the result,
5725
+ * mark any embed-time failure, and return the validated Float32 vector.
5726
+ *
5727
+ * Failure kinds: `no_provider`, `invalid_vector`, `float32_overflow`,
5728
+ * `provider_error`. All embed-time failures are marked via `markFailure`;
5729
+ * `no_provider` deliberately does not mark (spec §2.4).
5730
+ *
5731
+ * Storage errors are NOT possible here — that is `storeFactVector`'s domain.
5732
+ */
5733
+ async embedTextForFact(fact, ctx, opts) {
5734
+ const markFailures = opts?.markFailures !== false;
4834
5735
  if (typeof this.options.llmProvider.embed !== "function") return { ok: false, kind: "no_provider" };
4835
5736
  let tagsStr;
4836
5737
  if (Array.isArray(fact.tags)) {
@@ -4851,8 +5752,8 @@ var EmbeddingService = class {
4851
5752
  const vector = await this.options.llmProvider.embed(text);
4852
5753
  if (vector.length === 0 || !vector.every((v) => typeof v === "number" && isFinite(v))) {
4853
5754
  console.warn(`[WikiMemory] embedFact: embed() returned an invalid vector for ${fact.id}; skipping.`);
4854
- this.reportEmbed(ctx, fact, "embedding_failed", "invalid_vector");
4855
- await this.markFailure(fact.id, "invalid_vector");
5755
+ if (markFailures) this.reportEmbed(ctx, fact, "embedding_failed", "invalid_vector");
5756
+ if (markFailures) await this.markFailure(fact.id, "invalid_vector");
4856
5757
  return { ok: false, kind: "invalid_vector" };
4857
5758
  }
4858
5759
  float32Vector = new Float32Array(vector);
@@ -4865,16 +5766,24 @@ var EmbeddingService = class {
4865
5766
  }
4866
5767
  if (hasNonFinite) {
4867
5768
  console.warn(`[WikiMemory] embedFact: embed() returned values that overflow float32 for ${fact.id}; skipping.`);
4868
- this.reportEmbed(ctx, fact, "embedding_failed", "float32_overflow");
4869
- await this.markFailure(fact.id, "float32_overflow");
5769
+ if (markFailures) this.reportEmbed(ctx, fact, "embedding_failed", "float32_overflow");
5770
+ if (markFailures) await this.markFailure(fact.id, "float32_overflow");
4870
5771
  return { ok: false, kind: "float32_overflow" };
4871
5772
  }
4872
5773
  } catch (err) {
4873
5774
  console.warn(`[WikiMemory] embedFact failed for ${fact.id}:`, err);
4874
- this.reportEmbed(ctx, fact, "embedding_failed", "embed_threw");
4875
- await this.markFailure(fact.id, "provider_error");
5775
+ if (markFailures) this.reportEmbed(ctx, fact, "embedding_failed", "embed_threw");
5776
+ if (markFailures) await this.markFailure(fact.id, "provider_error");
4876
5777
  return { ok: false, kind: "provider_error" };
4877
5778
  }
5779
+ return { ok: true, vector: float32Vector };
5780
+ }
5781
+ /**
5782
+ * Storage half: persist the computed vector to the entry row, then fire the
5783
+ * `onEmbeddingPersisted` hook. Failure kinds: `storage_error` only — embed
5784
+ * failures do not originate here, so they cannot be marked as such (spec §3.2, D3).
5785
+ */
5786
+ async storeFactVector(fact, float32Vector, ctx) {
4878
5787
  try {
4879
5788
  await this.storeEmbeddingDimension(float32Vector.length);
4880
5789
  const blob = new Uint8Array(float32Vector.buffer);
@@ -5059,6 +5968,40 @@ function selectWithFloors(sortedRows, floors, maxResults) {
5059
5968
  return Array.from(selected).sort((a, b) => a - b).map((i) => sortedRows[i]);
5060
5969
  }
5061
5970
 
5971
+ // src/utils/budget.ts
5972
+ function factTokenCost(f) {
5973
+ return estimateTokens(`${f.title} ${f.body} ${f.tags.join(" ")}`);
5974
+ }
5975
+ function packFactsByBudget(items, budget, scores) {
5976
+ const candidates = items.length;
5977
+ if (candidates === 0) return { items: [], tokensUsed: 0, candidates: 0, packed: 0 };
5978
+ let top = items[0];
5979
+ let topCost = factTokenCost(top);
5980
+ if (topCost > budget) {
5981
+ const overhead = estimateTokens(`${top.title} ${top.tags.join(" ")}`);
5982
+ top = { ...top, body: safeSlice(top.body, 0, Math.max(0, (budget - overhead) * 4)) };
5983
+ topCost = factTokenCost(top);
5984
+ }
5985
+ const kept = [{ rank: 0, item: top }];
5986
+ let used = topCost;
5987
+ const rest = items.slice(1).map((item, i) => {
5988
+ const rank = i + 1;
5989
+ const cost = factTokenCost(item);
5990
+ const s = scores?.get(item.id);
5991
+ const value = typeof s === "number" && Number.isFinite(s) && s > 0 ? s : 1 / (rank + 1);
5992
+ return { rank, item, cost, density: value / Math.max(1, cost) };
5993
+ });
5994
+ rest.sort((a, b) => b.density - a.density || a.rank - b.rank);
5995
+ for (const r of rest) {
5996
+ if (used + r.cost <= budget) {
5997
+ kept.push({ rank: r.rank, item: r.item });
5998
+ used += r.cost;
5999
+ }
6000
+ }
6001
+ kept.sort((a, b) => a.rank - b.rank);
6002
+ return { items: kept.map((k) => k.item), tokensUsed: used, candidates, packed: kept.length };
6003
+ }
6004
+
5062
6005
  // src/services/RetrievalService.ts
5063
6006
  var EMPTY_ID_SET = /* @__PURE__ */ new Set();
5064
6007
  var RetrievalService = class {
@@ -5094,6 +6037,28 @@ var RetrievalService = class {
5094
6037
  const rawMaxResults = options?.maxResults ?? config?.maxResults ?? config?.maxFtsResults ?? 10;
5095
6038
  const maxResults = Number.isFinite(rawMaxResults) ? Math.max(0, Math.trunc(rawMaxResults)) : 10;
5096
6039
  const excludeDrafts = (options?.excludeDrafts ?? config?.excludeDrafts ?? false) === true;
6040
+ const rawAsOf = options?.asOf;
6041
+ if (rawAsOf !== void 0 && !(typeof rawAsOf === "number" && Number.isFinite(rawAsOf) && rawAsOf >= 0)) {
6042
+ throw new WikiInvalidReadOptions("asOf", "must be a finite epoch-ms number >= 0");
6043
+ }
6044
+ const tokenBudget = options?.tokenBudget;
6045
+ if (tokenBudget !== void 0 && !(typeof tokenBudget === "number" && Number.isFinite(tokenBudget) && tokenBudget >= 0)) {
6046
+ throw new WikiInvalidReadOptions("tokenBudget", "must be a finite number >= 0");
6047
+ }
6048
+ const applyBudget = (input) => {
6049
+ if (tokenBudget === void 0) return input;
6050
+ const packed = packFactsByBudget(input, Math.trunc(tokenBudget), scoreByFactId);
6051
+ emitDiagnostic(this.options, {
6052
+ code: "read_budget",
6053
+ operation: "read",
6054
+ trigger: "call",
6055
+ entityId: entityIds.length === 1 ? entityIds[0] : entityIds.join(","),
6056
+ detail: { candidates: packed.candidates, packed: packed.packed, tokensUsed: packed.tokensUsed }
6057
+ });
6058
+ return packed.items;
6059
+ };
6060
+ const liveMode = rawAsOf === void 0 ? "current" : "asOf";
6061
+ const liveAt = rawAsOf === void 0 ? Date.now() : Math.trunc(rawAsOf);
5097
6062
  const trimmedQuery = query.trim();
5098
6063
  const sanitizedTierFloors = trimmedQuery && exposeMetadata ? validateTierFloors(
5099
6064
  entityIds,
@@ -5113,8 +6078,10 @@ var RetrievalService = class {
5113
6078
  let usedEmbed = false;
5114
6079
  const scoredEntityIds = this._filterScoredEntities(entityIds, sanitizedTierWeights, options?.includeZeroWeightEntities);
5115
6080
  const draftIds = excludeDrafts && scoredEntityIds.length > 0 ? await this.entryRepo.findDraftIdsByEntityIds(scoredEntityIds) : EMPTY_ID_SET;
5116
- const draftPad = draftIds.size;
5117
- const padLimit = (n) => n >= Number.MAX_SAFE_INTEGER ? n : n + draftPad;
6081
+ const nonLiveIds = scoredEntityIds.length > 0 ? await this.entryRepo.findNonLiveIdsByEntityIds(scoredEntityIds, liveMode, liveAt) : EMPTY_ID_SET;
6082
+ const excludedIds = draftIds.size === 0 ? nonLiveIds : nonLiveIds.size === 0 ? draftIds : /* @__PURE__ */ new Set([...draftIds, ...nonLiveIds]);
6083
+ const excludedPad = excludedIds.size;
6084
+ const padLimit = (n) => n >= Number.MAX_SAFE_INTEGER ? n : n + excludedPad;
5118
6085
  if (scoredEntityIds.length === 0) {
5119
6086
  usedEmbed = true;
5120
6087
  } else if (!skipEmbed && typeof this.options.llmProvider.embed === "function") {
@@ -5148,7 +6115,7 @@ var RetrievalService = class {
5148
6115
  let miniSearchScores;
5149
6116
  if (effectivePreFilterLimit !== void 0) {
5150
6117
  populateCache = false;
5151
- const preResults = this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, Number.MAX_SAFE_INTEGER).filter((r) => !draftIds.has(r.id));
6118
+ const preResults = this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, Number.MAX_SAFE_INTEGER).filter((r) => !excludedIds.has(r.id));
5152
6119
  if (preResults.length === 0) {
5153
6120
  candidateRows = null;
5154
6121
  } else {
@@ -5170,9 +6137,9 @@ var RetrievalService = class {
5170
6137
  }
5171
6138
  } else {
5172
6139
  if (useRanker) {
5173
- candidateRows = this._withoutDrafts(await this.entryRepo.findMetadataByEntityIds(scoredEntityIds), draftIds);
6140
+ candidateRows = this._withoutIds(await this.entryRepo.findMetadataByEntityIds(scoredEntityIds), excludedIds);
5174
6141
  } else {
5175
- candidateRows = this._withoutDrafts(await this.entryRepo.findWithEmbeddingsByEntityIds(scoredEntityIds), draftIds);
6142
+ candidateRows = this._withoutIds(await this.entryRepo.findWithEmbeddingsByEntityIds(scoredEntityIds), excludedIds);
5176
6143
  }
5177
6144
  if (weight !== void 0 && weight < 1) {
5178
6145
  miniSearchScores = this.searchService.getMiniSearchScores(trimmedQuery, scoredEntityIds);
@@ -5457,7 +6424,7 @@ var RetrievalService = class {
5457
6424
  // read() re-sorts after applying tier weights
5458
6425
  });
5459
6426
  }
5460
- if (draftPad > 0) scored = scored.filter((s) => !draftIds.has(s.id));
6427
+ if (excludedPad > 0) scored = scored.filter((s) => !excludedIds.has(s.id));
5461
6428
  if (scored.length > 0) {
5462
6429
  scored = scored.map((row) => ({
5463
6430
  ...row,
@@ -5521,7 +6488,7 @@ var RetrievalService = class {
5521
6488
  if (!usedEmbed && scoredEntityIds.length > 0) {
5522
6489
  const hasActiveFloors = sanitizedTierFloors !== void 0 && Object.values(sanitizedTierFloors).some((f) => f > 0);
5523
6490
  const fallbackOversampledLimit = hasActiveFloors ? Number.MAX_SAFE_INTEGER : padLimit(Math.max(maxResults * 2, maxResults + 50));
5524
- const results = this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, fallbackOversampledLimit).filter((r) => !draftIds.has(r.id));
6491
+ const results = this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, fallbackOversampledLimit).filter((r) => !excludedIds.has(r.id));
5525
6492
  const candidates = results.map((r) => ({
5526
6493
  id: r.id,
5527
6494
  entity_id: r.entity_id,
@@ -5539,13 +6506,18 @@ var RetrievalService = class {
5539
6506
  }
5540
6507
  }
5541
6508
  }
6509
+ facts = applyBudget(facts);
5542
6510
  if (facts.length > 0) {
5543
6511
  const ids = facts.map((f) => f.id);
5544
6512
  const now = Date.now();
5545
6513
  await this.entryRepo.trackAccess(ids, now);
5546
6514
  }
5547
6515
  } else {
5548
- facts = excludeDrafts ? await this.entryRepo.findRecentByEntityIds(entityIds, maxResults, void 0, { excludeDrafts: true }) : await this.entryRepo.findRecentByEntityIds(entityIds, maxResults);
6516
+ facts = await this.entryRepo.findRecentByEntityIds(entityIds, maxResults, void 0, {
6517
+ ...excludeDrafts ? { excludeDrafts: true } : {},
6518
+ live: { mode: liveMode, t: liveAt }
6519
+ });
6520
+ facts = applyBudget(facts);
5549
6521
  }
5550
6522
  const eventsLimit = Math.min(10 * entityIds.length, 100);
5551
6523
  const [tasks, events] = await Promise.all([
@@ -5581,8 +6553,8 @@ var RetrievalService = class {
5581
6553
  _tieBreakSort(items) {
5582
6554
  items.sort((a, b) => this._compareScoredRows(a, b));
5583
6555
  }
5584
- _withoutDrafts(rows, draftIds) {
5585
- return draftIds.size === 0 ? rows : rows.filter((row) => !draftIds.has(row.id));
6556
+ _withoutIds(rows, excludeIds) {
6557
+ return excludeIds.size === 0 ? rows : rows.filter((row) => !excludeIds.has(row.id));
5586
6558
  }
5587
6559
  /**
5588
6560
  * Comparator for score + deterministic tie-break fields.
@@ -5664,6 +6636,18 @@ var WriteService = class {
5664
6636
  this.metadataRepo = metadataRepo;
5665
6637
  this.jobManager = jobManager;
5666
6638
  this.maintenanceService = maintenanceService;
6639
+ this.inflight = /* @__PURE__ */ new Set();
6640
+ }
6641
+ track(p) {
6642
+ this.inflight.add(p);
6643
+ p.finally(() => this.inflight.delete(p)).catch(() => {
6644
+ });
6645
+ }
6646
+ /** Await every background job this instance started (spec §6.2). Loops because a finishing job can start another. */
6647
+ async drain() {
6648
+ while (this.inflight.size > 0) {
6649
+ await Promise.allSettled([...this.inflight]);
6650
+ }
5667
6651
  }
5668
6652
  async write(entityId, event) {
5669
6653
  if (typeof entityId !== "string" || entityId.length === 0 || entityId.length > 200 || entityId.includes("\0")) {
@@ -5690,6 +6674,7 @@ var WriteService = class {
5690
6674
  }
5691
6675
  const id = generateId("evt_");
5692
6676
  const now = Date.now();
6677
+ const occurredAt = event.occurred_at == null ? null : assertEpochMs("event.occurred_at", event.occurred_at);
5693
6678
  let eventType = event.event_type;
5694
6679
  if (!["observation", "decision", "action", "outcome"].includes(eventType)) {
5695
6680
  eventType = "observation";
@@ -5700,8 +6685,14 @@ var WriteService = class {
5700
6685
  event_type: eventType,
5701
6686
  summary,
5702
6687
  related_entry_id: relatedEntryId,
5703
- created_at: now
6688
+ created_at: now,
6689
+ occurred_at: occurredAt
5704
6690
  };
6691
+ const deferred = this.options.config?.maintenance === "deferred";
6692
+ if (deferred) {
6693
+ await this.eventRepo.add(newEvent);
6694
+ return;
6695
+ }
5705
6696
  let shouldRunLibrarian = false;
5706
6697
  let librarianCount = 0;
5707
6698
  let prevMemoryCheckpoint = 0;
@@ -5723,27 +6714,41 @@ var WriteService = class {
5723
6714
  prevMemoryCheckpoint = memoryCheckpoint;
5724
6715
  await this.metadataRepo.updateCheckpoint(entityId, { memory: count }, tx);
5725
6716
  }
6717
+ } else {
6718
+ const tokenThreshold = this.options.config?.autoLibrarianTokenThreshold;
6719
+ if (typeof tokenThreshold === "number" && tokenThreshold > 0 && !this.jobManager.isBlocked("librarian", entityId)) {
6720
+ const wm = await this.metadataRepo.getLibrarianWatermark(entityId, tx);
6721
+ const chars = await this.eventRepo.sumSummaryCharsAfter(entityId, wm, tx);
6722
+ if (Math.ceil(chars / 4) >= tokenThreshold) {
6723
+ shouldRunLibrarian = true;
6724
+ librarianCount = count;
6725
+ prevMemoryCheckpoint = memoryCheckpoint;
6726
+ await this.metadataRepo.updateCheckpoint(entityId, { memory: count }, tx);
6727
+ }
6728
+ }
5726
6729
  }
5727
6730
  });
5728
6731
  if (shouldRunLibrarian) {
5729
6732
  try {
5730
6733
  this.jobManager.acquireLock("librarian", entityId);
5731
6734
  const stage = { job: "librarian" };
5732
- this.runLibrarianThenMaybeHeal(entityId, librarianCount, prevMemoryCheckpoint, stage).catch((err) => {
6735
+ const job = this.runLibrarianThenMaybeHeal(entityId, librarianCount, prevMemoryCheckpoint, stage).catch((err) => {
5733
6736
  console.error(err);
5734
6737
  this.reportBackgroundFailure(entityId, stage.job);
5735
6738
  }).finally(() => {
5736
6739
  this.jobManager.releaseLock("librarian", entityId);
5737
6740
  });
6741
+ this.track(job);
5738
6742
  } catch (e) {
5739
6743
  if (!(e instanceof WikiBusyError)) throw e;
5740
6744
  await this.metadataRepo.updateCheckpoint(entityId, { memory: prevMemoryCheckpoint }, this.db);
5741
6745
  }
5742
6746
  } else if (!this.jobManager.isBlocked("librarian", entityId)) {
5743
- this.maybeRunHeal(entityId, eventCount).catch((err) => {
6747
+ const healJob = this.maybeRunHeal(entityId, eventCount).catch((err) => {
5744
6748
  console.error(err);
5745
6749
  this.reportBackgroundFailure(entityId, "heal");
5746
6750
  });
6751
+ this.track(healJob);
5747
6752
  }
5748
6753
  }
5749
6754
  async runLibrarianThenMaybeHeal(entityId, currentEventCount, prevCheckpoint, stage) {
@@ -5790,6 +6795,6 @@ var WriteService = class {
5790
6795
  }
5791
6796
  };
5792
6797
 
5793
- export { BaseRepository, DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, DEFAULT_MAX_EMBED_CHARS, DiagnosticBuffer, EMBED_CHARS_CEILING, EmbeddingService, HEAL_ANCHORS_PER_CANDIDATE, HEAL_BATCH_SIZE, HEAL_MAX_FACT_BODY_CHARS_L3, HEAL_MAX_TASKS, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ImportExportService, IngestionService, JobManager, MaintenanceService, MetadataRepository, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, RetrievalService, SearchService, WikiBusyError, WikiDraftNotFound, WikiDuplicateHashError, WikiGraphNodeOwnershipConflict, WikiIngestEmptyError, WikiInvalidReadOptions, WikiParseError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiTransactionError, WriteService, __privateAdd, __privateGet, __privateSet, chunkText, configureRandomSource, edgeTripleAllowed, emptyManifest, entitySummaryMetaKey, extractSqliteCode, generateId, normalizeSourceHash, normalizeSourceRef, normalizeTitleKey, parseEmbedding, resolveEdgeDefinitions, resolveGrounding, resolveNodeType, safeSlice, typeSatisfies, validateInlineEdges, validateManifest };
5794
- //# sourceMappingURL=chunk-46X3HO77.mjs.map
5795
- //# sourceMappingURL=chunk-46X3HO77.mjs.map
6798
+ export { BaseRepository, DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, DEFAULT_MAX_EMBED_CHARS, DiagnosticBuffer, EMBED_CHARS_CEILING, EmbeddingService, HEAL_ANCHORS_PER_CANDIDATE, HEAL_BATCH_SIZE, HEAL_MAX_FACT_BODY_CHARS_L3, HEAL_MAX_TASKS, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ImportExportService, IngestionService, JobManager, MAX_EMBED_ATTEMPTS, MaintenanceService, MetadataRepository, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, RetrievalService, SearchService, TemporalService, UsageMeter, WikiBusyError, WikiDraftNotFound, WikiDuplicateHashError, WikiGraphNodeOwnershipConflict, WikiIngestEmptyError, WikiInvalidReadOptions, WikiParseError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiSupersedeError, WikiTransactionError, WriteService, __privateAdd, __privateGet, __privateSet, chunkText, configureRandomSource, edgeTripleAllowed, emptyManifest, entitySummaryMetaKey, extractSqliteCode, factTokenCost, generateId, isLiveAt, liveAtSql, normalizeSourceHash, normalizeSourceRef, normalizeTitleKey, packFactsByBudget, parseEmbedding, resolveEdgeDefinitions, resolveGrounding, resolveNodeType, safeSlice, typeSatisfies, validateInlineEdges, validateManifest };
6799
+ //# sourceMappingURL=chunk-J2TWKOEE.mjs.map
6800
+ //# sourceMappingURL=chunk-J2TWKOEE.mjs.map