@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.
- package/README.md +94 -0
- package/dist/{chunk-46X3HO77.mjs → chunk-J2TWKOEE.mjs} +1190 -185
- package/dist/chunk-J2TWKOEE.mjs.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1862 -285
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +606 -35
- package/dist/index.mjs.map +1 -1
- package/dist/{testing-BIFNLUva.d.mts → testing-Kb1uVj1E.d.mts} +594 -7
- package/dist/{testing-BIFNLUva.d.ts → testing-Kb1uVj1E.d.ts} +594 -7
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1139 -197
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +1 -1
- package/package.json +2 -2
- package/dist/chunk-46X3HO77.mjs.map +0 -1
|
@@ -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
|
|
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
|
-
|
|
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/
|
|
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
|
|
3292
|
-
|
|
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
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
5117
|
-
const
|
|
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) => !
|
|
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.
|
|
6140
|
+
candidateRows = this._withoutIds(await this.entryRepo.findMetadataByEntityIds(scoredEntityIds), excludedIds);
|
|
5174
6141
|
} else {
|
|
5175
|
-
candidateRows = this.
|
|
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 (
|
|
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) => !
|
|
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 =
|
|
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
|
-
|
|
5585
|
-
return
|
|
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-
|
|
5795
|
-
//# sourceMappingURL=chunk-
|
|
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
|