@equationalapplications/core-llm-wiki 7.7.7 → 7.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -0
- package/dist/{chunk-46X3HO77.mjs → chunk-M76CF2Q6.mjs} +1180 -185
- package/dist/chunk-M76CF2Q6.mjs.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1824 -285
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +575 -32
- package/dist/index.mjs.map +1 -1
- package/dist/{testing-BIFNLUva.d.mts → testing-CWZaXKLj.d.mts} +560 -4
- package/dist/{testing-BIFNLUva.d.ts → testing-CWZaXKLj.d.ts} +560 -4
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1129 -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;
|
|
@@ -1745,6 +1754,21 @@ function groundingEvidenceBlock(writer, cfg) {
|
|
|
1745
1754
|
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
1755
|
"evidence": ["exact substring copied from the SOURCE section"]`;
|
|
1747
1756
|
}
|
|
1757
|
+
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.
|
|
1758
|
+
Return ONLY a valid JSON object matching this schema:
|
|
1759
|
+
{
|
|
1760
|
+
"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" }],
|
|
1761
|
+
"tasks": [{ "description": "string", "priority": "number (0-10)" }]
|
|
1762
|
+
}
|
|
1763
|
+
One fact per claim. Every fact must name the single event it comes from in "source_event". Do not return markdown, just raw JSON.`;
|
|
1764
|
+
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:
|
|
1765
|
+
- "ADD": the candidate is new information; the existing facts are about something else.
|
|
1766
|
+
- "UPDATE": one existing fact states the same thing and the candidate adds detail. Give the merged "title" and "body" for that existing fact.
|
|
1767
|
+
- "SUPERSEDE": the candidate replaces an existing fact that is no longer true (moved, changed jobs, changed preference, reversed decision).
|
|
1768
|
+
- "NOOP": an existing fact already states the candidate.
|
|
1769
|
+
Return ONLY a valid JSON object matching this schema:
|
|
1770
|
+
{ "ops": [{ "item": 0, "op": "ADD|UPDATE|SUPERSEDE|NOOP", "target": "n1", "title": "UPDATE only", "body": "UPDATE only" }] }
|
|
1771
|
+
"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
1772
|
|
|
1749
1773
|
// src/utils/healConstants.ts
|
|
1750
1774
|
var HEAL_MAX_ANCHORS = 50;
|
|
@@ -1983,6 +2007,22 @@ ${JSON.stringify(facts, null, 2)}`
|
|
|
1983
2007
|
${JSON.stringify(facts, null, 2)}`
|
|
1984
2008
|
};
|
|
1985
2009
|
}
|
|
2010
|
+
/** Ops extract (spec §5.2): events only — no existing facts. */
|
|
2011
|
+
buildOpsExtractPrompt(events, ontologyContext) {
|
|
2012
|
+
const lines = events.map((l) => `${l.label} [${new Date(l.at).toISOString()}] (${l.event.event_type}) ${l.event.summary}`);
|
|
2013
|
+
const grounding = this.groundingFor("librarian");
|
|
2014
|
+
return {
|
|
2015
|
+
systemPrompt: this.appendGrounding(this.appendOntology(OPS_EXTRACT_SYSTEM_PROMPT, ontologyContext), "librarian"),
|
|
2016
|
+
userPrompt: `Events:
|
|
2017
|
+
${lines.join("\n")}`,
|
|
2018
|
+
...grounding ? { groundingCorpus: buildGroundingCorpus(events.map((l) => l.event.summary)) } : {}
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
/** Ops resolve (spec §5.4): only each item's own neighbours. */
|
|
2022
|
+
buildOpsResolvePrompt(items) {
|
|
2023
|
+
return { systemPrompt: OPS_RESOLVE_SYSTEM_PROMPT, userPrompt: `Items:
|
|
2024
|
+
${JSON.stringify({ items })}` };
|
|
2025
|
+
}
|
|
1986
2026
|
/**
|
|
1987
2027
|
* The system prompt each writer would send, without hydrating data (spec
|
|
1988
2028
|
* §8.3). `buildSystemPrompt` with no variables hydrates only ontology
|
|
@@ -2061,7 +2101,14 @@ var SEVERITY = {
|
|
|
2061
2101
|
grounding_missing: "warn",
|
|
2062
2102
|
grounding_failed: "warn",
|
|
2063
2103
|
classification_low_confidence: "info",
|
|
2064
|
-
classification_invalid: "warn"
|
|
2104
|
+
classification_invalid: "warn",
|
|
2105
|
+
llm_usage: "info",
|
|
2106
|
+
librarian_gate: "info",
|
|
2107
|
+
librarian_op_rejected: "warn",
|
|
2108
|
+
contradicts_document: "warn",
|
|
2109
|
+
resolve_failed: "warn",
|
|
2110
|
+
read_budget: "info",
|
|
2111
|
+
event_retention_held: "info"
|
|
2065
2112
|
};
|
|
2066
2113
|
var MESSAGE = {
|
|
2067
2114
|
ingest_chunk_failed: "One or more document chunks failed to parse or generate.",
|
|
@@ -2076,7 +2123,14 @@ var MESSAGE = {
|
|
|
2076
2123
|
grounding_missing: "A fact carried no qualifying evidence and was stored as a draft.",
|
|
2077
2124
|
grounding_failed: "A fact carried evidence not found in its source and was stored as a draft.",
|
|
2078
2125
|
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."
|
|
2126
|
+
classification_invalid: "A classifier answer was invalid and was not applied.",
|
|
2127
|
+
llm_usage: "An LLM call completed; token usage attached.",
|
|
2128
|
+
librarian_gate: "Librarian gate outcome counts for one pass.",
|
|
2129
|
+
librarian_op_rejected: "A librarian reconciliation op was invalid and the candidate was added instead.",
|
|
2130
|
+
contradicts_document: "A candidate contradicted a document fact and was stored as a draft.",
|
|
2131
|
+
resolve_failed: "Reconciliation failed for a candidate; it was stored as a draft.",
|
|
2132
|
+
read_budget: "A read was packed to a token budget.",
|
|
2133
|
+
event_retention_held: "Event pruning was held back because the librarian has not processed those events yet."
|
|
2080
2134
|
};
|
|
2081
2135
|
var warnedNonFunction = /* @__PURE__ */ new WeakSet();
|
|
2082
2136
|
function copyDetail(detail) {
|
|
@@ -2162,6 +2216,79 @@ function edgeDropDiagnostic(drop, ctx) {
|
|
|
2162
2216
|
return { code: "edge_dropped", operation: ctx.operation, trigger: ctx.trigger, entityId: ctx.entityId, detail };
|
|
2163
2217
|
}
|
|
2164
2218
|
|
|
2219
|
+
// src/utils/usage.ts
|
|
2220
|
+
function estimateTokens(text) {
|
|
2221
|
+
return Math.ceil(text.length / 4);
|
|
2222
|
+
}
|
|
2223
|
+
var UsageMeter = class {
|
|
2224
|
+
constructor(budget) {
|
|
2225
|
+
this.used = 0;
|
|
2226
|
+
this.estimated = false;
|
|
2227
|
+
if (budget !== void 0 && (typeof budget !== "number" || !Number.isFinite(budget) || budget < 0)) {
|
|
2228
|
+
throw new TypeError("tokenBudget must be a finite number >= 0");
|
|
2229
|
+
}
|
|
2230
|
+
this.budget = budget;
|
|
2231
|
+
}
|
|
2232
|
+
get remaining() {
|
|
2233
|
+
return this.budget === void 0 ? Infinity : Math.max(0, this.budget - this.used);
|
|
2234
|
+
}
|
|
2235
|
+
fits(estimate) {
|
|
2236
|
+
return estimate <= this.remaining;
|
|
2237
|
+
}
|
|
2238
|
+
record(r) {
|
|
2239
|
+
this.used += r.inputTokens + r.outputTokens;
|
|
2240
|
+
if (r.estimated) this.estimated = true;
|
|
2241
|
+
}
|
|
2242
|
+
};
|
|
2243
|
+
var WikiBudgetExhausted = class _WikiBudgetExhausted extends Error {
|
|
2244
|
+
constructor(requiredEstimate, remaining) {
|
|
2245
|
+
super(`LLM call needs ~${requiredEstimate} input tokens; ${remaining} remain in the budget.`);
|
|
2246
|
+
this.name = "WikiBudgetExhausted";
|
|
2247
|
+
this.requiredEstimate = requiredEstimate;
|
|
2248
|
+
this.remaining = remaining;
|
|
2249
|
+
Object.setPrototypeOf(this, _WikiBudgetExhausted.prototype);
|
|
2250
|
+
}
|
|
2251
|
+
};
|
|
2252
|
+
|
|
2253
|
+
// src/utils/llmCall.ts
|
|
2254
|
+
function validUsage(u) {
|
|
2255
|
+
if (!u || typeof u !== "object") return false;
|
|
2256
|
+
const { inputTokens, outputTokens } = u;
|
|
2257
|
+
return [inputTokens, outputTokens].every((n) => typeof n === "number" && Number.isFinite(n) && n >= 0);
|
|
2258
|
+
}
|
|
2259
|
+
async function callLlm(options, ctx, prompts) {
|
|
2260
|
+
const promptEstimate = estimateTokens(prompts.systemPrompt) + estimateTokens(prompts.userPrompt);
|
|
2261
|
+
if (ctx.meter && !ctx.meter.fits(promptEstimate)) {
|
|
2262
|
+
throw new WikiBudgetExhausted(promptEstimate, ctx.meter.remaining);
|
|
2263
|
+
}
|
|
2264
|
+
const provider = options.llmProvider;
|
|
2265
|
+
const params = { systemPrompt: prompts.systemPrompt, userPrompt: prompts.userPrompt };
|
|
2266
|
+
let text;
|
|
2267
|
+
let reported;
|
|
2268
|
+
if (typeof provider.generateTextWithUsage === "function") {
|
|
2269
|
+
const result = await provider.generateTextWithUsage(params);
|
|
2270
|
+
if (!result || typeof result.text !== "string") {
|
|
2271
|
+
throw new TypeError("generateTextWithUsage() must resolve to { text: string, usage? }");
|
|
2272
|
+
}
|
|
2273
|
+
text = result.text;
|
|
2274
|
+
reported = validUsage(result.usage) ? result.usage : void 0;
|
|
2275
|
+
} else {
|
|
2276
|
+
text = await provider.generateText(params);
|
|
2277
|
+
}
|
|
2278
|
+
const record = reported ? { inputTokens: reported.inputTokens, outputTokens: reported.outputTokens, estimated: false } : { inputTokens: promptEstimate, outputTokens: estimateTokens(text), estimated: true };
|
|
2279
|
+
ctx.meter?.record(record);
|
|
2280
|
+
if (options.config?.reportLlmUsage === true) {
|
|
2281
|
+
emitDiagnostic(options, {
|
|
2282
|
+
code: "llm_usage",
|
|
2283
|
+
operation: ctx.operation,
|
|
2284
|
+
trigger: ctx.trigger,
|
|
2285
|
+
entityId: ctx.entityId,
|
|
2286
|
+
detail: { inputTokens: record.inputTokens, outputTokens: record.outputTokens, estimated: record.estimated }
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
return text;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2165
2292
|
// src/services/IngestionService.ts
|
|
2166
2293
|
var LLM_PASS_DIAGNOSTIC_CODES = ["ingest_chunk_failed", "fact_rejected", "fact_deduplicated"];
|
|
2167
2294
|
function dedupDiagnostic(entityId, sourceRef, locator) {
|
|
@@ -2238,7 +2365,7 @@ var IngestionService = class {
|
|
|
2238
2365
|
ontologyContext
|
|
2239
2366
|
);
|
|
2240
2367
|
try {
|
|
2241
|
-
const responseText = await this.options
|
|
2368
|
+
const responseText = await callLlm(this.options, { operation: "ingest", entityId, trigger: "call" }, { systemPrompt, userPrompt });
|
|
2242
2369
|
const result2 = parseJsonResponse(responseText);
|
|
2243
2370
|
const rawFacts = Array.isArray(result2.facts) ? result2.facts : [];
|
|
2244
2371
|
const facts = [];
|
|
@@ -2908,6 +3035,37 @@ var MetadataRepository = class extends BaseRepository {
|
|
|
2908
3035
|
[entityId]
|
|
2909
3036
|
);
|
|
2910
3037
|
}
|
|
3038
|
+
/**
|
|
3039
|
+
* Read the op-based librarian's high-water mark. Returns `null` when the
|
|
3040
|
+
* entity has never had its watermark set.
|
|
3041
|
+
*/
|
|
3042
|
+
async getLibrarianWatermark(entityId, tx) {
|
|
3043
|
+
const executor = this.getExecutor(tx);
|
|
3044
|
+
const row = await executor.getFirstAsync(
|
|
3045
|
+
`SELECT librarian_watermark_at, librarian_watermark_id FROM ${this.prefix}checkpoints WHERE entity_id = ?`,
|
|
3046
|
+
[entityId]
|
|
3047
|
+
);
|
|
3048
|
+
if (!row || row.librarian_watermark_at == null || row.librarian_watermark_id == null) {
|
|
3049
|
+
return null;
|
|
3050
|
+
}
|
|
3051
|
+
return { at: row.librarian_watermark_at, id: row.librarian_watermark_id };
|
|
3052
|
+
}
|
|
3053
|
+
/**
|
|
3054
|
+
* Write the op-based librarian's high-water mark. Uses `ON CONFLICT`
|
|
3055
|
+
* `DO UPDATE` so existing `memory_checkpoint` / `heal_checkpoint` rows
|
|
3056
|
+
* are preserved verbatim.
|
|
3057
|
+
*/
|
|
3058
|
+
async setLibrarianWatermark(entityId, wm, tx) {
|
|
3059
|
+
const executor = this.getExecutor(tx);
|
|
3060
|
+
await executor.runAsync(
|
|
3061
|
+
`INSERT INTO ${this.prefix}checkpoints (entity_id, memory_checkpoint, heal_checkpoint, librarian_watermark_at, librarian_watermark_id)
|
|
3062
|
+
VALUES (?, 0, 0, ?, ?)
|
|
3063
|
+
ON CONFLICT(entity_id) DO UPDATE SET
|
|
3064
|
+
librarian_watermark_at = excluded.librarian_watermark_at,
|
|
3065
|
+
librarian_watermark_id = excluded.librarian_watermark_id`,
|
|
3066
|
+
[entityId, wm.at, wm.id]
|
|
3067
|
+
);
|
|
3068
|
+
}
|
|
2911
3069
|
// META TABLE METHODS
|
|
2912
3070
|
async getMeta(key, tx) {
|
|
2913
3071
|
const executor = this.getExecutor(tx);
|
|
@@ -3080,6 +3238,8 @@ async function runBatched(args) {
|
|
|
3080
3238
|
const results = [];
|
|
3081
3239
|
const skipped = [];
|
|
3082
3240
|
let batches = 0;
|
|
3241
|
+
const settled = /* @__PURE__ */ new Set();
|
|
3242
|
+
let budgetStop;
|
|
3083
3243
|
let batchSize = initialBatchSize(maxOutputTokens);
|
|
3084
3244
|
const trim = async (candidate) => {
|
|
3085
3245
|
const whole = await buildPrompt(candidate);
|
|
@@ -3113,6 +3273,7 @@ async function runBatched(args) {
|
|
|
3113
3273
|
} else {
|
|
3114
3274
|
const reason = fromCall && !isTruncationError(err) && !isConfigError(err) ? "call_error" : "non_convergent";
|
|
3115
3275
|
skipped.push({ item: batch[0], reason });
|
|
3276
|
+
settled.add(batch[0]);
|
|
3116
3277
|
onSkip?.(batch[0], err);
|
|
3117
3278
|
}
|
|
3118
3279
|
return;
|
|
@@ -3121,7 +3282,7 @@ async function runBatched(args) {
|
|
|
3121
3282
|
const mid = Math.ceil(batch.length / 2);
|
|
3122
3283
|
if (mid < batchSize) batchSize = mid;
|
|
3123
3284
|
let i = 0;
|
|
3124
|
-
while (i < batch.length) {
|
|
3285
|
+
while (i < batch.length && !budgetStop) {
|
|
3125
3286
|
const size = Math.min(batchSize, batch.length - i);
|
|
3126
3287
|
const trimmed = await trim(batch.slice(i, i + size));
|
|
3127
3288
|
await attempt(trimmed.batch, trimmed.prompts);
|
|
@@ -3129,6 +3290,7 @@ async function runBatched(args) {
|
|
|
3129
3290
|
}
|
|
3130
3291
|
};
|
|
3131
3292
|
const attempt = async (batch, prebuilt, attemptLevel = 0) => {
|
|
3293
|
+
if (budgetStop) return;
|
|
3132
3294
|
if (batch.length === 0) return;
|
|
3133
3295
|
const prompts = prebuilt && attemptLevel === 0 ? prebuilt : await buildPrompt(batch, attemptLevel);
|
|
3134
3296
|
batches++;
|
|
@@ -3136,6 +3298,10 @@ async function runBatched(args) {
|
|
|
3136
3298
|
try {
|
|
3137
3299
|
responseText = await call(prompts);
|
|
3138
3300
|
} catch (err) {
|
|
3301
|
+
if (err instanceof WikiBudgetExhausted) {
|
|
3302
|
+
budgetStop = { requiredEstimate: err.requiredEstimate };
|
|
3303
|
+
return;
|
|
3304
|
+
}
|
|
3139
3305
|
await onFailure(batch, err, attemptLevel, true);
|
|
3140
3306
|
return;
|
|
3141
3307
|
}
|
|
@@ -3147,19 +3313,781 @@ async function runBatched(args) {
|
|
|
3147
3313
|
return;
|
|
3148
3314
|
}
|
|
3149
3315
|
results.push(result);
|
|
3316
|
+
for (const item of batch) settled.add(item);
|
|
3150
3317
|
};
|
|
3151
3318
|
let index = 0;
|
|
3152
|
-
while (index < items.length) {
|
|
3319
|
+
while (index < items.length && !budgetStop) {
|
|
3153
3320
|
const { batch, prompts } = await trim(items.slice(index, index + batchSize));
|
|
3154
3321
|
index += batch.length;
|
|
3155
3322
|
await attempt(batch, prompts, 0);
|
|
3156
3323
|
}
|
|
3157
|
-
|
|
3324
|
+
const unattempted = budgetStop ? items.filter((item) => !settled.has(item)) : [];
|
|
3325
|
+
return { results, skipped, batches, unattempted, ...budgetStop ? { budgetStop } : {} };
|
|
3158
3326
|
}
|
|
3159
3327
|
|
|
3160
|
-
// src/services/
|
|
3328
|
+
// src/services/librarian/constants.ts
|
|
3161
3329
|
var FUZZY_THRESHOLD = 0.5;
|
|
3162
3330
|
var MIN_TOKENS_TO_QUALIFY = 3;
|
|
3331
|
+
|
|
3332
|
+
// src/services/librarian/legacy.ts
|
|
3333
|
+
var runLegacyLibrarianPass = async (deps, ctx) => {
|
|
3334
|
+
const { entityId, trigger } = ctx;
|
|
3335
|
+
const promptOverride = ctx.promptOverride;
|
|
3336
|
+
const events = await deps.eventRepo.getRecent(entityId, 50);
|
|
3337
|
+
const currentFactsRows = await deps.entryRepo.findRecentByEntityId(entityId, 100);
|
|
3338
|
+
const currentFacts = currentFactsRows.map((f) => {
|
|
3339
|
+
const { embedding: _embedding, embedding_blob: _blob, ...rest } = f;
|
|
3340
|
+
return {
|
|
3341
|
+
...rest,
|
|
3342
|
+
tags: typeof rest.tags === "string" ? JSON.parse(rest.tags) : rest.tags
|
|
3343
|
+
};
|
|
3344
|
+
});
|
|
3345
|
+
const ontologyContext = await deps.ontologyService?.buildPromptContext(entityId) ?? null;
|
|
3346
|
+
const promptEvents = events.reverse();
|
|
3347
|
+
const librarianGrounding = deps.promptService.groundingFor("librarian");
|
|
3348
|
+
const { systemPrompt, userPrompt, groundingCorpus: librarianCorpus = [] } = deps.promptService.buildLibrarianPrompt(
|
|
3349
|
+
promptEvents,
|
|
3350
|
+
currentFacts,
|
|
3351
|
+
promptOverride,
|
|
3352
|
+
ontologyContext
|
|
3353
|
+
);
|
|
3354
|
+
let responseText;
|
|
3355
|
+
try {
|
|
3356
|
+
responseText = await callLlm(
|
|
3357
|
+
deps.options,
|
|
3358
|
+
{ operation: "librarian", entityId, trigger, meter: ctx.meter },
|
|
3359
|
+
{ systemPrompt, userPrompt }
|
|
3360
|
+
);
|
|
3361
|
+
} catch (err) {
|
|
3362
|
+
if (err instanceof WikiBudgetExhausted) {
|
|
3363
|
+
return { processedThrough: null, factsWritten: 0, budgetStop: { requiredEstimate: err.requiredEstimate } };
|
|
3364
|
+
}
|
|
3365
|
+
throw err;
|
|
3366
|
+
}
|
|
3367
|
+
const result = parseJsonResponse(responseText);
|
|
3368
|
+
const facts = Array.isArray(result.facts) ? result.facts : [];
|
|
3369
|
+
const tasks = Array.isArray(result.tasks) ? result.tasks : [];
|
|
3370
|
+
const ontologyUpdates = result.ontology_updates;
|
|
3371
|
+
const diagBuffer = new DiagnosticBuffer();
|
|
3372
|
+
const diagBase = { entityId, operation: "librarian", trigger };
|
|
3373
|
+
const validFacts = [];
|
|
3374
|
+
const validFactItemIndexes = [];
|
|
3375
|
+
facts.forEach((raw, itemIndex) => {
|
|
3376
|
+
const valid = validateFact(raw);
|
|
3377
|
+
if (valid) {
|
|
3378
|
+
validFacts.push(valid);
|
|
3379
|
+
validFactItemIndexes.push(itemIndex);
|
|
3380
|
+
} else {
|
|
3381
|
+
diagBuffer.push({ ...diagBase, code: "fact_rejected", detail: { itemIndex, reason: factRejectionReason(raw) } });
|
|
3382
|
+
}
|
|
3383
|
+
});
|
|
3384
|
+
const validTasks = [];
|
|
3385
|
+
tasks.forEach((raw, itemIndex) => {
|
|
3386
|
+
const valid = validateTask(raw);
|
|
3387
|
+
if (valid) validTasks.push(valid);
|
|
3388
|
+
else diagBuffer.push({ ...diagBase, code: "task_rejected", detail: { itemIndex, reason: taskRejectionReason(raw) } });
|
|
3389
|
+
});
|
|
3390
|
+
const now = Date.now();
|
|
3391
|
+
const insertedFacts = [];
|
|
3392
|
+
await deps.db.withTransactionAsync(async (tx) => {
|
|
3393
|
+
let { mode, manifest } = await deps.ontologyService?.getEffectiveState(entityId, tx) ?? { mode: "off", manifest: { node_types: [], edge_types: [] } };
|
|
3394
|
+
if (mode === "emergent" && ontologyUpdates && deps.ontologyService) {
|
|
3395
|
+
manifest = await deps.ontologyService.mergeEmergentUpdates(entityId, ontologyUpdates, tx);
|
|
3396
|
+
}
|
|
3397
|
+
const titleIndex = /* @__PURE__ */ new Map();
|
|
3398
|
+
for (const existing of currentFactsRows) {
|
|
3399
|
+
titleIndex.set(normalizeTitleKey(existing.title), {
|
|
3400
|
+
id: existing.id,
|
|
3401
|
+
okf_type: existing.okf_type ?? null
|
|
3402
|
+
});
|
|
3403
|
+
}
|
|
3404
|
+
const factsForDedupe = await deps.entryRepo.findRecentByEntityId(entityId, 100, tx);
|
|
3405
|
+
const pendingEdges = [];
|
|
3406
|
+
for (const [k, fact] of validFacts.entries()) {
|
|
3407
|
+
const newTokens = titleTokens(fact.title);
|
|
3408
|
+
let skip = false;
|
|
3409
|
+
if (newTokens.size >= MIN_TOKENS_TO_QUALIFY) {
|
|
3410
|
+
for (const existing of factsForDedupe) {
|
|
3411
|
+
if (existing.source_type !== "librarian_inferred") continue;
|
|
3412
|
+
const existingTokens = titleTokens(existing.title);
|
|
3413
|
+
if (existingTokens.size >= MIN_TOKENS_TO_QUALIFY) {
|
|
3414
|
+
if (jaccardScore(newTokens, existingTokens) >= FUZZY_THRESHOLD) {
|
|
3415
|
+
skip = true;
|
|
3416
|
+
break;
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
if (skip) {
|
|
3422
|
+
diagBuffer.push({ ...diagBase, code: "fact_deduplicated", detail: { itemIndex: validFactItemIndexes[k], reason: "fuzzy_title" } });
|
|
3423
|
+
continue;
|
|
3424
|
+
}
|
|
3425
|
+
const ontologyFact = fact;
|
|
3426
|
+
const id = generateId("fact_");
|
|
3427
|
+
const validationDrops = [];
|
|
3428
|
+
const normalized = deps.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false, drops: validationDrops }) ?? { okf_type: null, edges: [] };
|
|
3429
|
+
for (const drop of validationDrops) diagBuffer.push(edgeDropDiagnostic(drop, { ...diagBase, factId: id }));
|
|
3430
|
+
const grounding = librarianGrounding ? groundingOutcome(checkGrounding(fact.evidence, librarianCorpus, librarianGrounding), now) : null;
|
|
3431
|
+
if (grounding?.diagnostic) {
|
|
3432
|
+
diagBuffer.push({ ...diagBase, code: grounding.diagnostic.code, detail: { factId: id, itemIndex: validFactItemIndexes[k], reason: grounding.diagnostic.reason } });
|
|
3433
|
+
}
|
|
3434
|
+
const factObj = {
|
|
3435
|
+
id,
|
|
3436
|
+
entity_id: entityId,
|
|
3437
|
+
title: fact.title,
|
|
3438
|
+
body: fact.body,
|
|
3439
|
+
tags: fact.tags,
|
|
3440
|
+
confidence: fact.confidence,
|
|
3441
|
+
source_type: "librarian_inferred",
|
|
3442
|
+
source_hash: null,
|
|
3443
|
+
source_ref: null,
|
|
3444
|
+
created_at: now,
|
|
3445
|
+
updated_at: now,
|
|
3446
|
+
last_accessed_at: null,
|
|
3447
|
+
access_count: 0,
|
|
3448
|
+
deleted_at: null,
|
|
3449
|
+
okf_type: normalized.okf_type,
|
|
3450
|
+
...grounding?.trust
|
|
3451
|
+
};
|
|
3452
|
+
await deps.entryRepo.upsert(factObj, tx);
|
|
3453
|
+
insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
|
|
3454
|
+
factsForDedupe.push(factObj);
|
|
3455
|
+
titleIndex.set(normalizeTitleKey(fact.title), { id, okf_type: normalized.okf_type });
|
|
3456
|
+
if (normalized.edges.length > 0) {
|
|
3457
|
+
pendingEdges.push({ sourceId: id, sourceType: normalized.okf_type, edges: normalized.edges });
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
for (const item of pendingEdges) {
|
|
3461
|
+
const resolveDrops = [];
|
|
3462
|
+
await deps.ontologyService?.resolveAndPersistEdges(
|
|
3463
|
+
entityId,
|
|
3464
|
+
item.sourceId,
|
|
3465
|
+
item.sourceType,
|
|
3466
|
+
item.edges ?? [],
|
|
3467
|
+
manifest,
|
|
3468
|
+
titleIndex,
|
|
3469
|
+
tx,
|
|
3470
|
+
now,
|
|
3471
|
+
resolveDrops
|
|
3472
|
+
);
|
|
3473
|
+
for (const drop of resolveDrops) diagBuffer.push(edgeDropDiagnostic(drop, diagBase));
|
|
3474
|
+
}
|
|
3475
|
+
for (const task of validTasks) {
|
|
3476
|
+
const id = generateId("task_");
|
|
3477
|
+
const taskObj = {
|
|
3478
|
+
id,
|
|
3479
|
+
entity_id: entityId,
|
|
3480
|
+
description: task.description,
|
|
3481
|
+
status: "pending",
|
|
3482
|
+
priority: task.priority,
|
|
3483
|
+
created_at: now,
|
|
3484
|
+
updated_at: now,
|
|
3485
|
+
resolved_at: null,
|
|
3486
|
+
deleted_at: null
|
|
3487
|
+
};
|
|
3488
|
+
await deps.taskRepo.upsert(taskObj, tx);
|
|
3489
|
+
}
|
|
3490
|
+
});
|
|
3491
|
+
diagBuffer.flush(deps.options);
|
|
3492
|
+
await deps.searchService.syncEntries(entityId, insertedFacts.map((f) => f.id));
|
|
3493
|
+
for (const fact of insertedFacts) {
|
|
3494
|
+
await deps.embeddingService.embedFact(fact, { operation: "librarian", trigger });
|
|
3495
|
+
}
|
|
3496
|
+
deps.searchService.evictCache(entityId);
|
|
3497
|
+
const processedThrough = events.length > 0 ? { at: events[events.length - 1].created_at, id: events[events.length - 1].id } : null;
|
|
3498
|
+
const out = { processedThrough, factsWritten: insertedFacts.length };
|
|
3499
|
+
return out;
|
|
3500
|
+
};
|
|
3501
|
+
|
|
3502
|
+
// src/services/librarian/ops/text.ts
|
|
3503
|
+
var OPS_NEIGHBOUR_BODY_CHARS = 400;
|
|
3504
|
+
function normalizeFactText(title, body) {
|
|
3505
|
+
return `${title} ${body}`.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim();
|
|
3506
|
+
}
|
|
3507
|
+
function labelEvents(events) {
|
|
3508
|
+
return events.map((event, i) => ({ label: `e${i + 1}`, event, at: event.occurred_at ?? event.created_at }));
|
|
3509
|
+
}
|
|
3510
|
+
function parseValidFrom(v) {
|
|
3511
|
+
if (typeof v === "number") return Number.isFinite(v) && v >= 0 ? Math.trunc(v) : void 0;
|
|
3512
|
+
if (typeof v === "string") {
|
|
3513
|
+
const t = Date.parse(v);
|
|
3514
|
+
return Number.isFinite(t) && t >= 0 ? t : void 0;
|
|
3515
|
+
}
|
|
3516
|
+
return void 0;
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
// src/services/librarian/ops/extract.ts
|
|
3520
|
+
var OPS_MAX_EVENTS_PER_PASS = 50;
|
|
3521
|
+
var OPS_MAX_EXTRACT_CHARS = 24e3;
|
|
3522
|
+
var RESOLVE_RESERVE_FACTOR = 2;
|
|
3523
|
+
var estimatePrompt = (p) => estimateTokens(p.systemPrompt) + estimateTokens(p.userPrompt);
|
|
3524
|
+
async function selectBatch(deps, entityId, wm, meter) {
|
|
3525
|
+
const events = await deps.eventRepo.getAfter(entityId, wm, OPS_MAX_EVENTS_PER_PASS);
|
|
3526
|
+
if (events.length === 0) return { batch: [] };
|
|
3527
|
+
let n = 0;
|
|
3528
|
+
let chars = 0;
|
|
3529
|
+
for (const e of events) {
|
|
3530
|
+
if (n > 0 && chars + e.summary.length > OPS_MAX_EXTRACT_CHARS) break;
|
|
3531
|
+
chars += e.summary.length;
|
|
3532
|
+
n++;
|
|
3533
|
+
}
|
|
3534
|
+
let batch = labelEvents(events.slice(0, n));
|
|
3535
|
+
if (meter && meter.budget !== void 0) {
|
|
3536
|
+
const ontologyContext = await deps.ontologyService?.buildPromptContext(entityId) ?? null;
|
|
3537
|
+
for (; ; ) {
|
|
3538
|
+
const need = RESOLVE_RESERVE_FACTOR * estimatePrompt(deps.promptService.buildOpsExtractPrompt(batch, ontologyContext));
|
|
3539
|
+
if (meter.fits(need)) break;
|
|
3540
|
+
if (batch.length === 1) return { budgetStop: { requiredEstimate: need } };
|
|
3541
|
+
batch = labelEvents(events.slice(0, Math.max(1, Math.floor(batch.length / 2))));
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
return { batch };
|
|
3545
|
+
}
|
|
3546
|
+
async function runExtract(deps, ctx, batch) {
|
|
3547
|
+
const { entityId, trigger } = ctx;
|
|
3548
|
+
const ontologyContext = await deps.ontologyService?.buildPromptContext(entityId) ?? null;
|
|
3549
|
+
const prompt = deps.promptService.buildOpsExtractPrompt(batch, ontologyContext);
|
|
3550
|
+
const text = await callLlm(deps.options, { operation: "librarian", entityId, trigger, meter: ctx.meter }, prompt);
|
|
3551
|
+
const parsed = parseJsonResponse(text);
|
|
3552
|
+
const base = { entityId, operation: "librarian", trigger };
|
|
3553
|
+
const diagnostics = [];
|
|
3554
|
+
const byLabel = new Map(batch.map((l) => [l.label, l]));
|
|
3555
|
+
const newestAt = batch[batch.length - 1].at;
|
|
3556
|
+
const candidates = [];
|
|
3557
|
+
(Array.isArray(parsed.facts) ? parsed.facts : []).forEach((raw, index) => {
|
|
3558
|
+
const fact = validateFact(raw);
|
|
3559
|
+
if (!fact) {
|
|
3560
|
+
diagnostics.push({ ...base, code: "fact_rejected", detail: { itemIndex: index, reason: factRejectionReason(raw) } });
|
|
3561
|
+
return;
|
|
3562
|
+
}
|
|
3563
|
+
const r = raw;
|
|
3564
|
+
const sourceLabel = typeof r.source_event === "string" && byLabel.has(r.source_event) ? r.source_event : null;
|
|
3565
|
+
const validFrom = parseValidFrom(r.valid_from) ?? (sourceLabel ? byLabel.get(sourceLabel).at : newestAt);
|
|
3566
|
+
candidates.push({ index, fact, sourceLabel, validFrom });
|
|
3567
|
+
});
|
|
3568
|
+
const tasks = [];
|
|
3569
|
+
(Array.isArray(parsed.tasks) ? parsed.tasks : []).forEach((raw, index) => {
|
|
3570
|
+
const task = validateTask(raw);
|
|
3571
|
+
if (task) tasks.push(task);
|
|
3572
|
+
else diagnostics.push({ ...base, code: "task_rejected", detail: { itemIndex: index, reason: taskRejectionReason(raw) } });
|
|
3573
|
+
});
|
|
3574
|
+
return {
|
|
3575
|
+
batch,
|
|
3576
|
+
candidates,
|
|
3577
|
+
tasks,
|
|
3578
|
+
...parsed.ontology_updates ? { ontologyUpdates: parsed.ontology_updates } : {},
|
|
3579
|
+
groundingCorpus: prompt.groundingCorpus ?? null,
|
|
3580
|
+
diagnostics
|
|
3581
|
+
};
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3584
|
+
// src/services/librarian/ops/gate.ts
|
|
3585
|
+
var clamp01 = (v, d) => typeof v === "number" && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : d;
|
|
3586
|
+
function resolveGateConfig(cfg) {
|
|
3587
|
+
const kRaw = cfg?.k;
|
|
3588
|
+
const k = typeof kRaw === "number" && Number.isFinite(kRaw) ? Math.min(20, Math.max(1, Math.trunc(kRaw))) : 5;
|
|
3589
|
+
const dupThreshold = clamp01(cfg?.dupThreshold, 0.89);
|
|
3590
|
+
const novelThreshold = clamp01(cfg?.novelThreshold, 0.3);
|
|
3591
|
+
if (novelThreshold > dupThreshold) throw new TypeError("librarian.gate: novelThreshold must be <= dupThreshold");
|
|
3592
|
+
return { k, dupThreshold, novelThreshold };
|
|
3593
|
+
}
|
|
3594
|
+
function titlesMatch(a, b) {
|
|
3595
|
+
const ta = titleTokens(a);
|
|
3596
|
+
const tb = titleTokens(b);
|
|
3597
|
+
if (ta.size < MIN_TOKENS_TO_QUALIFY || tb.size < MIN_TOKENS_TO_QUALIFY) return false;
|
|
3598
|
+
return jaccardScore(ta, tb) >= FUZZY_THRESHOLD;
|
|
3599
|
+
}
|
|
3600
|
+
function classifyCandidate(candidate, neighbours, cfg, mode) {
|
|
3601
|
+
const key = normalizeFactText(candidate.title, candidate.body);
|
|
3602
|
+
const same = neighbours.find((n) => normalizeFactText(n.title, n.body) === key);
|
|
3603
|
+
if (same) return { kind: "noop", target: same.id };
|
|
3604
|
+
if (neighbours.length === 0) return { kind: "add" };
|
|
3605
|
+
if (mode === "keyword") return { kind: "ambiguous" };
|
|
3606
|
+
const best = neighbours[0];
|
|
3607
|
+
const score = best.score ?? 0;
|
|
3608
|
+
if (score >= cfg.dupThreshold && titlesMatch(candidate.title, best.title)) return { kind: "noop", target: best.id };
|
|
3609
|
+
if (score < cfg.novelThreshold) return { kind: "add" };
|
|
3610
|
+
return { kind: "ambiguous" };
|
|
3611
|
+
}
|
|
3612
|
+
async function gateCandidates(deps, ctx, candidates, cfg) {
|
|
3613
|
+
const { entityId, trigger } = ctx;
|
|
3614
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3615
|
+
const unique = [];
|
|
3616
|
+
const inBatchDuplicates = [];
|
|
3617
|
+
for (const c of candidates) {
|
|
3618
|
+
const key = normalizeFactText(c.fact.title, c.fact.body);
|
|
3619
|
+
if (seen.has(key)) inBatchDuplicates.push(c);
|
|
3620
|
+
else {
|
|
3621
|
+
seen.add(key);
|
|
3622
|
+
unique.push(c);
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
const nonLive = await deps.entryRepo.findNonLiveIdsByEntityIds([entityId], "current", Date.now());
|
|
3626
|
+
const canEmbed = typeof deps.options.llmProvider.embed === "function";
|
|
3627
|
+
const stored = [];
|
|
3628
|
+
if (canEmbed && unique.length > 0) {
|
|
3629
|
+
for (const row of await deps.entryRepo.findWithEmbeddingsByEntityIds([entityId])) {
|
|
3630
|
+
if (nonLive.has(row.id)) continue;
|
|
3631
|
+
const vec = parseEmbedding(row.embedding_blob, row.embedding);
|
|
3632
|
+
if (vec) stored.push({ id: row.id, vec });
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
const gated = [];
|
|
3636
|
+
for (const c of unique) {
|
|
3637
|
+
let ranked = null;
|
|
3638
|
+
let vector = null;
|
|
3639
|
+
let mode = "keyword";
|
|
3640
|
+
if (canEmbed) {
|
|
3641
|
+
const r = await deps.embeddingService.embedTextForFact(
|
|
3642
|
+
{ id: "candidate", entity_id: entityId, title: c.fact.title, body: c.fact.body, tags: c.fact.tags },
|
|
3643
|
+
void 0,
|
|
3644
|
+
{ markFailures: false }
|
|
3645
|
+
);
|
|
3646
|
+
if (r.ok) {
|
|
3647
|
+
vector = r.vector;
|
|
3648
|
+
if (stored.length > 0 && stored.every((s) => s.vec.length === r.vector.length)) {
|
|
3649
|
+
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);
|
|
3650
|
+
mode = "vector";
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
if (!ranked) {
|
|
3655
|
+
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 }));
|
|
3656
|
+
}
|
|
3657
|
+
const facts = ranked.length > 0 ? await deps.entryRepo.findByIds(ranked.map((r) => r.id), [entityId]) : [];
|
|
3658
|
+
const byId = new Map(facts.map((f) => [f.id, f]));
|
|
3659
|
+
const neighbours = ranked.filter((r) => byId.has(r.id)).map((r, i) => {
|
|
3660
|
+
const f = byId.get(r.id);
|
|
3661
|
+
return { id: f.id, ref: `n${i + 1}`, title: f.title, body: f.body, source_type: f.source_type, score: r.score };
|
|
3662
|
+
});
|
|
3663
|
+
gated.push({ candidate: c, neighbours, decision: classifyCandidate(c.fact, neighbours, cfg, mode), vector });
|
|
3664
|
+
}
|
|
3665
|
+
const count = (k) => gated.filter((g) => g.decision.kind === k).length;
|
|
3666
|
+
const gateDiagnostic = {
|
|
3667
|
+
code: "librarian_gate",
|
|
3668
|
+
operation: "librarian",
|
|
3669
|
+
trigger,
|
|
3670
|
+
entityId,
|
|
3671
|
+
detail: { gateNoop: count("noop"), gateAdd: count("add"), gateAmbiguous: count("ambiguous") }
|
|
3672
|
+
};
|
|
3673
|
+
return { gated, inBatchDuplicates, gateDiagnostic };
|
|
3674
|
+
}
|
|
3675
|
+
|
|
3676
|
+
// src/services/librarian/ops/resolve.ts
|
|
3677
|
+
var OPS_RESOLVE_MAX_PROMPT_CHARS = 4e4;
|
|
3678
|
+
function validateOp(raw, aliases) {
|
|
3679
|
+
if (!raw || typeof raw !== "object") return null;
|
|
3680
|
+
const r = raw;
|
|
3681
|
+
if (r.op === "ADD") return { op: "ADD" };
|
|
3682
|
+
if (r.op !== "UPDATE" && r.op !== "SUPERSEDE" && r.op !== "NOOP") return null;
|
|
3683
|
+
const targetId = typeof r.target === "string" ? aliases.get(r.target) : void 0;
|
|
3684
|
+
if (!targetId) return null;
|
|
3685
|
+
if (r.op === "NOOP") return { op: "NOOP", targetId };
|
|
3686
|
+
if (r.op === "SUPERSEDE") {
|
|
3687
|
+
const validFrom = parseValidFrom(r.valid_from);
|
|
3688
|
+
return validFrom === void 0 ? { op: "SUPERSEDE", targetId } : { op: "SUPERSEDE", targetId, validFrom };
|
|
3689
|
+
}
|
|
3690
|
+
const update = { op: "UPDATE", targetId };
|
|
3691
|
+
if (typeof r.title === "string" && r.title.trim() !== "") update.title = clip(r.title, 80);
|
|
3692
|
+
if (typeof r.body === "string" && r.body.trim() !== "") update.body = clip(r.body, 800);
|
|
3693
|
+
return update;
|
|
3694
|
+
}
|
|
3695
|
+
var aliasMapOf = (g) => new Map(g.neighbours.map((n) => [n.ref, n.id]));
|
|
3696
|
+
async function runResolve(deps, ctx, ambiguous) {
|
|
3697
|
+
const out = { ops: /* @__PURE__ */ new Map(), rejected: [], failed: [] };
|
|
3698
|
+
if (ambiguous.length === 0) return out;
|
|
3699
|
+
const { entityId, trigger } = ctx;
|
|
3700
|
+
const outcome = await runBatched({
|
|
3701
|
+
items: ambiguous,
|
|
3702
|
+
buildPrompt: (batch) => deps.promptService.buildOpsResolvePrompt(
|
|
3703
|
+
batch.map((g, i) => ({
|
|
3704
|
+
item: i,
|
|
3705
|
+
candidate: { title: g.candidate.fact.title, body: g.candidate.fact.body },
|
|
3706
|
+
existing: g.neighbours.map((n) => ({
|
|
3707
|
+
ref: n.ref,
|
|
3708
|
+
title: n.title,
|
|
3709
|
+
body: safeSlice(n.body, 0, OPS_NEIGHBOUR_BODY_CHARS)
|
|
3710
|
+
}))
|
|
3711
|
+
}))
|
|
3712
|
+
),
|
|
3713
|
+
call: (prompts) => callLlm(deps.options, { operation: "librarian", entityId, trigger, meter: ctx.meter }, prompts),
|
|
3714
|
+
parse: (text, batch) => {
|
|
3715
|
+
const parsed = parseJsonResponse(text);
|
|
3716
|
+
if (!Array.isArray(parsed.ops)) throw new Error("resolve: ops missing");
|
|
3717
|
+
return { batch, ops: parsed.ops };
|
|
3718
|
+
},
|
|
3719
|
+
maxPromptChars: OPS_RESOLVE_MAX_PROMPT_CHARS,
|
|
3720
|
+
maxOutputTokens: deps.options.llmProvider.maxOutputTokens
|
|
3721
|
+
});
|
|
3722
|
+
if (outcome.budgetStop) return { ...out, budgetStop: outcome.budgetStop };
|
|
3723
|
+
for (const { batch, ops } of outcome.results) {
|
|
3724
|
+
const byItem = /* @__PURE__ */ new Map();
|
|
3725
|
+
for (const raw of ops) {
|
|
3726
|
+
const item = raw?.item;
|
|
3727
|
+
if (typeof item === "number" && Number.isInteger(item) && item >= 0 && item < batch.length && !byItem.has(item)) {
|
|
3728
|
+
byItem.set(item, raw);
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
batch.forEach((g, i) => {
|
|
3732
|
+
const op = validateOp(byItem.get(i), aliasMapOf(g));
|
|
3733
|
+
if (op) out.ops.set(g, op);
|
|
3734
|
+
else out.rejected.push(g);
|
|
3735
|
+
});
|
|
3736
|
+
}
|
|
3737
|
+
if (outcome.skipped.some((s) => s.reason === "call_error")) {
|
|
3738
|
+
throw new Error("resolve: transient provider error; retry the pass");
|
|
3739
|
+
}
|
|
3740
|
+
for (const s of outcome.skipped) out.failed.push(s.item);
|
|
3741
|
+
return out;
|
|
3742
|
+
}
|
|
3743
|
+
|
|
3744
|
+
// src/utils/temporal.ts
|
|
3745
|
+
function liveAtSql(mode, alias = "") {
|
|
3746
|
+
const from = mode === "asOf" ? `COALESCE(${alias}valid_from, ${alias}created_at) <= ?` : `(${alias}valid_from IS NULL OR ${alias}valid_from <= ?)`;
|
|
3747
|
+
return `(${from} AND (${alias}valid_to IS NULL OR ${alias}valid_to > ?))`;
|
|
3748
|
+
}
|
|
3749
|
+
function isLiveAt(f, mode, t) {
|
|
3750
|
+
if (f.deleted_at != null) return false;
|
|
3751
|
+
const from = mode === "asOf" ? f.valid_from ?? f.created_at : f.valid_from;
|
|
3752
|
+
if (from != null && from > t) return false;
|
|
3753
|
+
return f.valid_to == null || f.valid_to > t;
|
|
3754
|
+
}
|
|
3755
|
+
function assertEpochMs(name, v) {
|
|
3756
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < 0) {
|
|
3757
|
+
throw new TypeError(`${name} must be a finite epoch-ms number >= 0`);
|
|
3758
|
+
}
|
|
3759
|
+
return Math.trunc(v);
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3762
|
+
// src/services/TemporalService.ts
|
|
3763
|
+
var HISTORY_MAX_DEPTH = 100;
|
|
3764
|
+
var TemporalService = class {
|
|
3765
|
+
constructor(db, options, entryRepo, searchService, embeddingService) {
|
|
3766
|
+
this.db = db;
|
|
3767
|
+
this.options = options;
|
|
3768
|
+
this.entryRepo = entryRepo;
|
|
3769
|
+
this.searchService = searchService;
|
|
3770
|
+
this.embeddingService = embeddingService;
|
|
3771
|
+
}
|
|
3772
|
+
/**
|
|
3773
|
+
* Link `oldId` → `newId` inside the caller's transaction. `newId` must
|
|
3774
|
+
* already exist. Throws WikiSupersedeError on any rule violation; the
|
|
3775
|
+
* caller's transaction then rolls back.
|
|
3776
|
+
*/
|
|
3777
|
+
async supersedeInTx(tx, entityId, oldId, newId, t, now) {
|
|
3778
|
+
const old = await this.entryRepo.findTemporalRow(oldId, tx);
|
|
3779
|
+
if (!old || old.deleted_at != null) throw new WikiSupersedeError("not_found");
|
|
3780
|
+
if (old.entity_id !== entityId) throw new WikiSupersedeError("cross_entity");
|
|
3781
|
+
if (old.superseded_by != null) throw new WikiSupersedeError("already_superseded");
|
|
3782
|
+
if (old.source_type === "immutable_document") throw new WikiSupersedeError("immutable_target");
|
|
3783
|
+
if (newId === oldId) throw new WikiSupersedeError("cycle");
|
|
3784
|
+
const repl = await this.entryRepo.findTemporalRow(newId, tx);
|
|
3785
|
+
if (!repl || repl.deleted_at != null) throw new WikiSupersedeError("not_found");
|
|
3786
|
+
if (repl.entity_id !== entityId) throw new WikiSupersedeError("cross_entity");
|
|
3787
|
+
if (repl.superseded_by != null) throw new WikiSupersedeError("already_superseded");
|
|
3788
|
+
const { predecessors } = await this.entryRepo.findSupersessionChainIds(entityId, oldId, HISTORY_MAX_DEPTH, tx);
|
|
3789
|
+
if (predecessors.includes(newId)) throw new WikiSupersedeError("cycle");
|
|
3790
|
+
await this.entryRepo.setTemporal(oldId, entityId, { valid_to: t, superseded_by: newId, superseded_at: now }, tx);
|
|
3791
|
+
if (repl.valid_from == null) {
|
|
3792
|
+
await this.entryRepo.setTemporal(newId, entityId, { valid_from: t }, tx);
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
async supersede(entityId, oldId, replacement, options) {
|
|
3796
|
+
const now = Date.now();
|
|
3797
|
+
const t = options?.validFrom === void 0 ? now : assertEpochMs("options.validFrom", options.validFrom);
|
|
3798
|
+
let created = null;
|
|
3799
|
+
if (typeof replacement !== "string") {
|
|
3800
|
+
if (typeof replacement?.title !== "string" || replacement.title.trim() === "" || typeof replacement.body !== "string") {
|
|
3801
|
+
throw new TypeError("supersede(): replacement must be a fact id or { title, body }");
|
|
3802
|
+
}
|
|
3803
|
+
created = {
|
|
3804
|
+
id: generateId("fact_"),
|
|
3805
|
+
entity_id: entityId,
|
|
3806
|
+
title: replacement.title,
|
|
3807
|
+
body: replacement.body,
|
|
3808
|
+
tags: Array.isArray(replacement.tags) ? replacement.tags : [],
|
|
3809
|
+
confidence: replacement.confidence ?? "certain",
|
|
3810
|
+
source_type: replacement.source_type ?? "user_stated",
|
|
3811
|
+
source_hash: null,
|
|
3812
|
+
source_ref: null,
|
|
3813
|
+
created_at: now,
|
|
3814
|
+
updated_at: now,
|
|
3815
|
+
last_accessed_at: null,
|
|
3816
|
+
access_count: 0,
|
|
3817
|
+
deleted_at: null
|
|
3818
|
+
};
|
|
3819
|
+
}
|
|
3820
|
+
const newId = typeof replacement === "string" ? replacement : created.id;
|
|
3821
|
+
const newValidFrom = typeof replacement !== "string" && replacement.valid_from !== void 0 ? assertEpochMs("replacement.valid_from", replacement.valid_from) : void 0;
|
|
3822
|
+
await this.db.withTransactionAsync(async (tx) => {
|
|
3823
|
+
if (created) {
|
|
3824
|
+
await this.entryRepo.upsert(created, tx);
|
|
3825
|
+
if (newValidFrom !== void 0) {
|
|
3826
|
+
await this.entryRepo.setTemporal(created.id, entityId, { valid_from: newValidFrom }, tx);
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
await this.supersedeInTx(tx, entityId, oldId, newId, t, now);
|
|
3830
|
+
});
|
|
3831
|
+
await this.searchService.syncEntries(entityId, [oldId, newId]);
|
|
3832
|
+
if (created) {
|
|
3833
|
+
await this.embeddingService.embedFact(
|
|
3834
|
+
{ id: created.id, entity_id: entityId, title: created.title, body: created.body, tags: created.tags },
|
|
3835
|
+
{ operation: "supersede", trigger: "call" }
|
|
3836
|
+
);
|
|
3837
|
+
}
|
|
3838
|
+
this.searchService.evictCache(entityId);
|
|
3839
|
+
return { newId };
|
|
3840
|
+
}
|
|
3841
|
+
async history(entityId, factId) {
|
|
3842
|
+
const start = await this.entryRepo.findTemporalRow(factId);
|
|
3843
|
+
if (!start || start.entity_id !== entityId || start.deleted_at != null) return [];
|
|
3844
|
+
const { predecessors, successors } = await this.entryRepo.findSupersessionChainIds(entityId, factId, HISTORY_MAX_DEPTH);
|
|
3845
|
+
const facts = await this.entryRepo.findByIds([...predecessors, factId, ...successors], [entityId]);
|
|
3846
|
+
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));
|
|
3847
|
+
}
|
|
3848
|
+
};
|
|
3849
|
+
|
|
3850
|
+
// src/services/librarian/ops/apply.ts
|
|
3851
|
+
async function applyOps(deps, ctx, input) {
|
|
3852
|
+
const { entityId, trigger } = ctx;
|
|
3853
|
+
const diagBase = { entityId, operation: "librarian", trigger };
|
|
3854
|
+
const grounding = deps.promptService.groundingFor("librarian");
|
|
3855
|
+
const temporal = new TemporalService(deps.db, deps.options, deps.entryRepo, deps.searchService, deps.embeddingService);
|
|
3856
|
+
const rejected = new Set(input.rejected);
|
|
3857
|
+
const failed = new Set(input.failed);
|
|
3858
|
+
const written = [];
|
|
3859
|
+
const updatedIds = [];
|
|
3860
|
+
const now = Date.now();
|
|
3861
|
+
await deps.db.withTransactionAsync(async (tx) => {
|
|
3862
|
+
let { mode, manifest } = await deps.ontologyService?.getEffectiveState(entityId, tx) ?? { mode: "off", manifest: { node_types: [], edge_types: [] } };
|
|
3863
|
+
if (mode === "emergent" && input.ontologyUpdates && deps.ontologyService) {
|
|
3864
|
+
manifest = await deps.ontologyService.mergeEmergentUpdates(entityId, input.ontologyUpdates, tx);
|
|
3865
|
+
}
|
|
3866
|
+
const titleIndex = /* @__PURE__ */ new Map();
|
|
3867
|
+
for (const f of await deps.entryRepo.findRecentByEntityId(entityId, 100, tx)) {
|
|
3868
|
+
titleIndex.set(normalizeTitleKey(f.title), { id: f.id, okf_type: f.okf_type ?? null });
|
|
3869
|
+
}
|
|
3870
|
+
const pendingEdges = [];
|
|
3871
|
+
const insert = async (g, draft, validFrom, overrides) => {
|
|
3872
|
+
const fact = g.candidate.fact;
|
|
3873
|
+
const title = overrides?.title ?? fact.title;
|
|
3874
|
+
const body = overrides?.body ?? fact.body;
|
|
3875
|
+
const id = generateId("fact_");
|
|
3876
|
+
const drops = [];
|
|
3877
|
+
const normalized = deps.ontologyService?.validateAndNormalizeFact(fact, manifest, { strict: false, drops }) ?? { okf_type: null, edges: [] };
|
|
3878
|
+
for (const d of drops) input.diagnostics.push(edgeDropDiagnostic(d, { ...diagBase, factId: id }));
|
|
3879
|
+
const g2 = grounding ? groundingOutcome(checkGrounding(fact.evidence, input.groundingCorpus ?? [], grounding), now) : null;
|
|
3880
|
+
if (g2?.diagnostic) {
|
|
3881
|
+
input.diagnostics.push({
|
|
3882
|
+
...diagBase,
|
|
3883
|
+
code: g2.diagnostic.code,
|
|
3884
|
+
detail: { factId: id, itemIndex: g.candidate.index, reason: g2.diagnostic.reason }
|
|
3885
|
+
});
|
|
3886
|
+
}
|
|
3887
|
+
const row = {
|
|
3888
|
+
id,
|
|
3889
|
+
entity_id: entityId,
|
|
3890
|
+
title,
|
|
3891
|
+
body,
|
|
3892
|
+
tags: fact.tags,
|
|
3893
|
+
confidence: fact.confidence,
|
|
3894
|
+
source_type: "librarian_inferred",
|
|
3895
|
+
source_hash: null,
|
|
3896
|
+
source_ref: null,
|
|
3897
|
+
created_at: now,
|
|
3898
|
+
updated_at: now,
|
|
3899
|
+
last_accessed_at: null,
|
|
3900
|
+
access_count: 0,
|
|
3901
|
+
deleted_at: null,
|
|
3902
|
+
okf_type: normalized.okf_type,
|
|
3903
|
+
...g2?.trust,
|
|
3904
|
+
...draft ? { lifecycle_status: "draft" } : {}
|
|
3905
|
+
};
|
|
3906
|
+
await deps.entryRepo.upsert(row, tx);
|
|
3907
|
+
await deps.entryRepo.setTemporal(id, entityId, { valid_from: validFrom }, tx);
|
|
3908
|
+
titleIndex.set(normalizeTitleKey(title), { id, okf_type: normalized.okf_type });
|
|
3909
|
+
if (normalized.edges.length > 0) pendingEdges.push({ sourceId: id, sourceType: normalized.okf_type, edges: normalized.edges });
|
|
3910
|
+
const textChanged = title !== fact.title || body !== fact.body;
|
|
3911
|
+
written.push({ id, vector: textChanged ? null : g.vector, fact: { id, entity_id: entityId, title, body, tags: fact.tags } });
|
|
3912
|
+
return id;
|
|
3913
|
+
};
|
|
3914
|
+
for (const g of input.gated) {
|
|
3915
|
+
const idx = g.candidate.index;
|
|
3916
|
+
const vf = g.candidate.validFrom;
|
|
3917
|
+
if (g.decision.kind === "noop") continue;
|
|
3918
|
+
if (g.decision.kind === "add") {
|
|
3919
|
+
await insert(g, false, vf);
|
|
3920
|
+
continue;
|
|
3921
|
+
}
|
|
3922
|
+
if (failed.has(g)) {
|
|
3923
|
+
const id = await insert(g, true, vf);
|
|
3924
|
+
input.diagnostics.push({ ...diagBase, code: "resolve_failed", detail: { factId: id, itemIndex: idx } });
|
|
3925
|
+
continue;
|
|
3926
|
+
}
|
|
3927
|
+
const op = input.ops.get(g);
|
|
3928
|
+
if (rejected.has(g) || !op) {
|
|
3929
|
+
await insert(g, false, vf);
|
|
3930
|
+
input.diagnostics.push({ ...diagBase, code: "librarian_op_rejected", detail: { itemIndex: idx, reason: "invalid_op" } });
|
|
3931
|
+
continue;
|
|
3932
|
+
}
|
|
3933
|
+
if (op.op === "NOOP") continue;
|
|
3934
|
+
if (op.op === "ADD") {
|
|
3935
|
+
await insert(g, false, vf);
|
|
3936
|
+
continue;
|
|
3937
|
+
}
|
|
3938
|
+
const target = g.neighbours.find((n) => n.id === op.targetId);
|
|
3939
|
+
if (op.op === "UPDATE" && target.source_type === "librarian_inferred") {
|
|
3940
|
+
const [existing] = await deps.entryRepo.findByIds([op.targetId], [entityId], tx);
|
|
3941
|
+
if (existing) {
|
|
3942
|
+
const newTitle = op.title ?? g.candidate.fact.title;
|
|
3943
|
+
await deps.entryRepo.upsert({
|
|
3944
|
+
...existing,
|
|
3945
|
+
title: newTitle,
|
|
3946
|
+
body: op.body ?? g.candidate.fact.body,
|
|
3947
|
+
updated_at: now
|
|
3948
|
+
}, tx);
|
|
3949
|
+
titleIndex.delete(normalizeTitleKey(existing.title));
|
|
3950
|
+
titleIndex.set(normalizeTitleKey(newTitle), { id: existing.id, okf_type: existing.okf_type ?? null });
|
|
3951
|
+
updatedIds.push(existing.id);
|
|
3952
|
+
continue;
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
const supersedeFrom = op.op === "SUPERSEDE" && op.validFrom !== void 0 ? op.validFrom : vf;
|
|
3956
|
+
const overrides = op.op === "UPDATE" ? { title: op.title, body: op.body } : void 0;
|
|
3957
|
+
if (target.source_type === "immutable_document") {
|
|
3958
|
+
const id = await insert(g, true, supersedeFrom, overrides);
|
|
3959
|
+
input.diagnostics.push({ ...diagBase, code: "contradicts_document", detail: { factId: id, itemIndex: idx } });
|
|
3960
|
+
continue;
|
|
3961
|
+
}
|
|
3962
|
+
const newId = await insert(g, false, supersedeFrom, overrides);
|
|
3963
|
+
try {
|
|
3964
|
+
await temporal.supersedeInTx(tx, entityId, op.targetId, newId, supersedeFrom, now);
|
|
3965
|
+
} catch (err) {
|
|
3966
|
+
if (!(err instanceof WikiSupersedeError)) throw err;
|
|
3967
|
+
input.diagnostics.push({
|
|
3968
|
+
...diagBase,
|
|
3969
|
+
code: "librarian_op_rejected",
|
|
3970
|
+
detail: { factId: newId, itemIndex: idx, reason: err.reason }
|
|
3971
|
+
});
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
for (const item of pendingEdges) {
|
|
3975
|
+
const resolveDrops = [];
|
|
3976
|
+
await deps.ontologyService?.resolveAndPersistEdges(
|
|
3977
|
+
entityId,
|
|
3978
|
+
item.sourceId,
|
|
3979
|
+
item.sourceType,
|
|
3980
|
+
item.edges,
|
|
3981
|
+
manifest,
|
|
3982
|
+
titleIndex,
|
|
3983
|
+
tx,
|
|
3984
|
+
now,
|
|
3985
|
+
resolveDrops
|
|
3986
|
+
);
|
|
3987
|
+
for (const d of resolveDrops) input.diagnostics.push(edgeDropDiagnostic(d, diagBase));
|
|
3988
|
+
}
|
|
3989
|
+
for (const task of input.tasks) {
|
|
3990
|
+
const taskObj = {
|
|
3991
|
+
id: generateId("task_"),
|
|
3992
|
+
entity_id: entityId,
|
|
3993
|
+
description: task.description,
|
|
3994
|
+
status: "pending",
|
|
3995
|
+
priority: task.priority,
|
|
3996
|
+
created_at: now,
|
|
3997
|
+
updated_at: now,
|
|
3998
|
+
resolved_at: null,
|
|
3999
|
+
deleted_at: null
|
|
4000
|
+
};
|
|
4001
|
+
await deps.taskRepo.upsert(taskObj, tx);
|
|
4002
|
+
}
|
|
4003
|
+
});
|
|
4004
|
+
return { written, updatedIds };
|
|
4005
|
+
}
|
|
4006
|
+
|
|
4007
|
+
// src/services/librarian/ops/index.ts
|
|
4008
|
+
var runOpsLibrarianPass = async (deps, ctx) => {
|
|
4009
|
+
const { entityId, trigger } = ctx;
|
|
4010
|
+
const cfg = resolveGateConfig(deps.options.config?.librarian?.gate);
|
|
4011
|
+
await deps.seedWatermark(entityId);
|
|
4012
|
+
const wm = await deps.getWatermark(entityId);
|
|
4013
|
+
const sel = await selectBatch(deps, entityId, wm, ctx.meter);
|
|
4014
|
+
if ("budgetStop" in sel) return { processedThrough: null, factsWritten: 0, budgetStop: sel.budgetStop };
|
|
4015
|
+
if (sel.batch.length === 0) return { processedThrough: null, factsWritten: 0 };
|
|
4016
|
+
let ex;
|
|
4017
|
+
try {
|
|
4018
|
+
ex = await runExtract(deps, ctx, sel.batch);
|
|
4019
|
+
} catch (err) {
|
|
4020
|
+
if (err instanceof WikiBudgetExhausted) {
|
|
4021
|
+
return { processedThrough: null, factsWritten: 0, budgetStop: { requiredEstimate: err.requiredEstimate } };
|
|
4022
|
+
}
|
|
4023
|
+
throw err;
|
|
4024
|
+
}
|
|
4025
|
+
const diagnostics = new DiagnosticBuffer();
|
|
4026
|
+
for (const d of ex.diagnostics) diagnostics.push(d);
|
|
4027
|
+
const { gated, inBatchDuplicates, gateDiagnostic } = await gateCandidates(deps, ctx, ex.candidates, cfg);
|
|
4028
|
+
for (const dup of inBatchDuplicates) {
|
|
4029
|
+
diagnostics.push({
|
|
4030
|
+
code: "fact_deduplicated",
|
|
4031
|
+
operation: "librarian",
|
|
4032
|
+
trigger,
|
|
4033
|
+
entityId,
|
|
4034
|
+
detail: { itemIndex: dup.index, reason: "in_batch" }
|
|
4035
|
+
});
|
|
4036
|
+
}
|
|
4037
|
+
diagnostics.push(gateDiagnostic);
|
|
4038
|
+
const res = await runResolve(deps, ctx, gated.filter((g) => g.decision.kind === "ambiguous"));
|
|
4039
|
+
if (res.budgetStop) {
|
|
4040
|
+
diagnostics.discard();
|
|
4041
|
+
return { processedThrough: null, factsWritten: 0, budgetStop: res.budgetStop };
|
|
4042
|
+
}
|
|
4043
|
+
const { written, updatedIds } = await applyOps(deps, ctx, {
|
|
4044
|
+
gated,
|
|
4045
|
+
ops: res.ops,
|
|
4046
|
+
rejected: res.rejected,
|
|
4047
|
+
failed: res.failed,
|
|
4048
|
+
tasks: ex.tasks,
|
|
4049
|
+
ontologyUpdates: ex.ontologyUpdates,
|
|
4050
|
+
groundingCorpus: ex.groundingCorpus,
|
|
4051
|
+
diagnostics
|
|
4052
|
+
});
|
|
4053
|
+
diagnostics.flush(deps.options);
|
|
4054
|
+
await deps.searchService.syncEntries(entityId, [...written.map((w) => w.id), ...updatedIds]);
|
|
4055
|
+
const embedCtx = { operation: "librarian", trigger };
|
|
4056
|
+
for (const w of written) {
|
|
4057
|
+
if (w.vector) await deps.embeddingService.storeFactVector(w.fact, w.vector, embedCtx);
|
|
4058
|
+
else await deps.embeddingService.embedFact(w.fact, embedCtx);
|
|
4059
|
+
}
|
|
4060
|
+
if (updatedIds.length > 0) {
|
|
4061
|
+
for (const f of await deps.entryRepo.findByIds(updatedIds, [entityId])) {
|
|
4062
|
+
await deps.embeddingService.embedFact(
|
|
4063
|
+
{ id: f.id, entity_id: f.entity_id, title: f.title, body: f.body, tags: f.tags },
|
|
4064
|
+
embedCtx
|
|
4065
|
+
);
|
|
4066
|
+
}
|
|
4067
|
+
}
|
|
4068
|
+
deps.searchService.evictCache(entityId);
|
|
4069
|
+
const last = sel.batch[sel.batch.length - 1].event;
|
|
4070
|
+
return {
|
|
4071
|
+
processedThrough: { at: last.created_at, id: last.id },
|
|
4072
|
+
factsWritten: written.length + updatedIds.length
|
|
4073
|
+
};
|
|
4074
|
+
};
|
|
4075
|
+
|
|
4076
|
+
// src/services/librarian/index.ts
|
|
4077
|
+
var STRATEGIES = /* @__PURE__ */ new Map([
|
|
4078
|
+
["legacy", runLegacyLibrarianPass],
|
|
4079
|
+
["ops", runOpsLibrarianPass]
|
|
4080
|
+
]);
|
|
4081
|
+
async function runLibrarianStrategy(deps, ctx) {
|
|
4082
|
+
const strategy = deps.options.config?.librarian?.strategy ?? "legacy";
|
|
4083
|
+
const fn = STRATEGIES.get(strategy);
|
|
4084
|
+
if (!fn) {
|
|
4085
|
+
throw new Error(`Unknown librarian strategy: ${strategy}`);
|
|
4086
|
+
}
|
|
4087
|
+
return fn(deps, ctx);
|
|
4088
|
+
}
|
|
4089
|
+
|
|
4090
|
+
// src/services/MaintenanceService.ts
|
|
3163
4091
|
var ONTOLOGY_BACKFILL_BATCH_SIZE = 25;
|
|
3164
4092
|
var ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS = 4e4;
|
|
3165
4093
|
var ONTOLOGY_BACKFILL_RECHECK_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
@@ -3232,11 +4160,23 @@ var MaintenanceService = class {
|
|
|
3232
4160
|
const vacuum = options?.vacuum ?? false;
|
|
3233
4161
|
this._validatePruneDuration(retainSoftDeletedFor, "retainSoftDeletedFor");
|
|
3234
4162
|
this._validatePruneDuration(retainEventsFor, "retainEventsFor");
|
|
4163
|
+
const pruneSupersededAfter = this.options.config?.pruneSupersededAfter ?? null;
|
|
4164
|
+
this._validatePruneDuration(pruneSupersededAfter, "pruneSupersededAfter");
|
|
3235
4165
|
const now = Date.now();
|
|
3236
4166
|
let syncedIds = [];
|
|
3237
4167
|
let deletedEntries = 0;
|
|
3238
4168
|
let deletedTasks = 0;
|
|
3239
4169
|
let deletedEvents = 0;
|
|
4170
|
+
if (pruneSupersededAfter !== null) {
|
|
4171
|
+
const expiredCutoff = now - pruneSupersededAfter * 864e5;
|
|
4172
|
+
const expiredIds = await this.entryRepo.findExpiredIds(entityId, expiredCutoff);
|
|
4173
|
+
if (expiredIds.length > 0) {
|
|
4174
|
+
await this.db.withTransactionAsync(async (tx) => {
|
|
4175
|
+
await this.entryRepo.softDeleteByIds(expiredIds, entityId, tx);
|
|
4176
|
+
});
|
|
4177
|
+
syncedIds = [...expiredIds];
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
3240
4180
|
if (retainSoftDeletedFor !== null) {
|
|
3241
4181
|
const cutoff = now - retainSoftDeletedFor * 864e5;
|
|
3242
4182
|
const entriesToDelete = await this.entryRepo.getPrunableMetadata(entityId, cutoff);
|
|
@@ -3252,7 +4192,7 @@ var MaintenanceService = class {
|
|
|
3252
4192
|
}
|
|
3253
4193
|
}
|
|
3254
4194
|
const succeededIds = succeeded.map((r) => r.id);
|
|
3255
|
-
syncedIds = succeededIds;
|
|
4195
|
+
syncedIds = [...syncedIds, ...succeededIds];
|
|
3256
4196
|
await this.db.withTransactionAsync(async (tx) => {
|
|
3257
4197
|
if (succeededIds.length > 0) {
|
|
3258
4198
|
deletedEntries = await this.entryRepo.bulkDeletePruned(entityId, cutoff, succeededIds, tx);
|
|
@@ -3288,8 +4228,18 @@ var MaintenanceService = class {
|
|
|
3288
4228
|
}
|
|
3289
4229
|
if (retainEventsFor !== null) {
|
|
3290
4230
|
const cutoff = now - retainEventsFor * 864e5;
|
|
3291
|
-
const
|
|
3292
|
-
|
|
4231
|
+
const guarded = this.options.config?.maintenance === "deferred" || this.options.config?.librarian?.strategy === "ops";
|
|
4232
|
+
if (guarded) {
|
|
4233
|
+
const wm = await this.metadataRepo.getLibrarianWatermark(entityId, this.db);
|
|
4234
|
+
deletedEvents = (await this.eventRepo.pruneThrough(entityId, cutoff, wm)).changes;
|
|
4235
|
+
const held = await this.eventRepo.countHeldBack(entityId, cutoff, wm);
|
|
4236
|
+
if (held > 0) {
|
|
4237
|
+
emitDiagnostic(this.options, { code: "event_retention_held", operation: "prune", trigger: "call", entityId, detail: { count: held } });
|
|
4238
|
+
}
|
|
4239
|
+
} else {
|
|
4240
|
+
const eventResult = await this.eventRepo.prune(entityId, cutoff);
|
|
4241
|
+
deletedEvents = eventResult.changes;
|
|
4242
|
+
}
|
|
3293
4243
|
}
|
|
3294
4244
|
if (vacuum) {
|
|
3295
4245
|
await this.metadataRepo.vacuum();
|
|
@@ -3519,155 +4469,72 @@ var MaintenanceService = class {
|
|
|
3519
4469
|
}
|
|
3520
4470
|
/** Core librarian pass (locks handled by {@link runLibrarian}). Package-internal orchestration hook. */
|
|
3521
4471
|
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 });
|
|
4472
|
+
await this.runLibrarianPass({ entityId, trigger, ...promptOverride !== void 0 ? { promptOverride } : {} });
|
|
4473
|
+
}
|
|
4474
|
+
/**
|
|
4475
|
+
* Seam between the lock wrapper and the strategy dispatcher (spec 2026-09-29
|
|
4476
|
+
* §5.1). Builds the strategy deps from `this` and dispatches via
|
|
4477
|
+
* `runLibrarianStrategy` so PR-B can plug in `ops/` without touching this
|
|
4478
|
+
* class. Returns the strategy's `LibrarianResult` so deferred/budgeted
|
|
4479
|
+
* schedulers (PR-C) can advance watermarks; the existing void-returning
|
|
4480
|
+
* `doRunLibrarian` discards it.
|
|
4481
|
+
*
|
|
4482
|
+
* Spec §10.3: one place advances the watermark, for every strategy, only
|
|
4483
|
+
* after the pass resolved (its transaction committed) and never backwards.
|
|
4484
|
+
* A budget-stopped pass wrote nothing and must not move the watermark.
|
|
4485
|
+
*/
|
|
4486
|
+
async runLibrarianPass(ctx) {
|
|
4487
|
+
const result = await runLibrarianStrategy(this.librarianDeps(), ctx);
|
|
4488
|
+
if (result.processedThrough && !result.budgetStop) {
|
|
4489
|
+
await this.advanceLibrarianWatermark(ctx.entityId, result.processedThrough);
|
|
3669
4490
|
}
|
|
3670
|
-
|
|
4491
|
+
return result;
|
|
4492
|
+
}
|
|
4493
|
+
/**
|
|
4494
|
+
* Strategy deps shared between legacy and ops librarians (spec §5.1). The
|
|
4495
|
+
* ops strategy (PR-B Task 8) reads/anchors the watermark through these two
|
|
4496
|
+
* hooks so the dispatcher itself never reaches into the metadata table —
|
|
4497
|
+
* the watermark invariant (one place advances, never backwards) lives here.
|
|
4498
|
+
*/
|
|
4499
|
+
librarianDeps() {
|
|
4500
|
+
return {
|
|
4501
|
+
db: this.db,
|
|
4502
|
+
options: this.options,
|
|
4503
|
+
entryRepo: this.entryRepo,
|
|
4504
|
+
taskRepo: this.taskRepo,
|
|
4505
|
+
eventRepo: this.eventRepo,
|
|
4506
|
+
searchService: this.searchService,
|
|
4507
|
+
embeddingService: this.embeddingService,
|
|
4508
|
+
promptService: this.promptService,
|
|
4509
|
+
ontologyService: this.ontologyService,
|
|
4510
|
+
seedWatermark: (entityId) => this.seedLibrarianWatermark(entityId),
|
|
4511
|
+
getWatermark: (entityId) => this.metadataRepo.getLibrarianWatermark(entityId, this.db)
|
|
4512
|
+
};
|
|
4513
|
+
}
|
|
4514
|
+
/**
|
|
4515
|
+
* Persist `next` as the entity's librarian watermark when it is strictly
|
|
4516
|
+
* newer than the stored value, under the (created_at, id) ordering used by
|
|
4517
|
+
* the op-based librarian (spec §10.3). Never moves backwards.
|
|
4518
|
+
*/
|
|
4519
|
+
async advanceLibrarianWatermark(entityId, next) {
|
|
4520
|
+
const current = await this.metadataRepo.getLibrarianWatermark(entityId, this.db);
|
|
4521
|
+
const newer = !current || next.at > current.at || next.at === current.at && next.id > current.id;
|
|
4522
|
+
if (newer) await this.metadataRepo.setLibrarianWatermark(entityId, next, this.db);
|
|
4523
|
+
}
|
|
4524
|
+
/**
|
|
4525
|
+
* Spec §10.3: first ops pass on a legacy DB starts after what the count
|
|
4526
|
+
* checkpoint already covered. No-op when a watermark already exists or the
|
|
4527
|
+
* memory checkpoint is 0; otherwise anchor the watermark to the event at
|
|
4528
|
+
* offset `min(memory_checkpoint, totalEvents) - 1`.
|
|
4529
|
+
*/
|
|
4530
|
+
async seedLibrarianWatermark(entityId) {
|
|
4531
|
+
if (await this.metadataRepo.getLibrarianWatermark(entityId, this.db)) return;
|
|
4532
|
+
const cp = await this.metadataRepo.getCheckpoint(entityId, this.db);
|
|
4533
|
+
const memory = cp.memory ?? 0;
|
|
4534
|
+
if (memory <= 0) return;
|
|
4535
|
+
const total = await this.eventRepo.count(entityId);
|
|
4536
|
+
const ev = await this.eventRepo.getAtOffset(entityId, Math.min(memory, total) - 1);
|
|
4537
|
+
if (ev) await this.metadataRepo.setLibrarianWatermark(entityId, { at: ev.created_at, id: ev.id }, this.db);
|
|
3671
4538
|
}
|
|
3672
4539
|
/**
|
|
3673
4540
|
* Core heal pass (locks handled by {@link runHeal}). Package-internal orchestration hook.
|
|
@@ -3773,7 +4640,7 @@ var MaintenanceService = class {
|
|
|
3773
4640
|
if (groundingCorpus !== void 0) corpusByPrompt.set(prompts, groundingCorpus);
|
|
3774
4641
|
return prompts;
|
|
3775
4642
|
},
|
|
3776
|
-
call: (prompts) => this.options
|
|
4643
|
+
call: (prompts) => callLlm(this.options, { operation: "heal", entityId, trigger, meter: options?.meter }, prompts),
|
|
3777
4644
|
parse: (responseText, batch, prompts) => {
|
|
3778
4645
|
const result = parseJsonResponse(responseText);
|
|
3779
4646
|
return {
|
|
@@ -3877,9 +4744,10 @@ var MaintenanceService = class {
|
|
|
3877
4744
|
const callErrorIds = new Set(
|
|
3878
4745
|
outcome.skipped.filter((s) => s.reason === "call_error").map((s) => s.item.id)
|
|
3879
4746
|
);
|
|
4747
|
+
const unattemptedIds = new Set(outcome.unattempted.map((f) => f.id));
|
|
3880
4748
|
await this.entryRepo.markHealChecked(
|
|
3881
4749
|
[
|
|
3882
|
-
...healCandidates.map((f) => f.id).filter((id) => !callErrorIds.has(id)),
|
|
4750
|
+
...healCandidates.map((f) => f.id).filter((id) => !callErrorIds.has(id) && !unattemptedIds.has(id)),
|
|
3883
4751
|
...insertedFacts.map((f) => f.id)
|
|
3884
4752
|
],
|
|
3885
4753
|
entityId,
|
|
@@ -3931,7 +4799,8 @@ var MaintenanceService = class {
|
|
|
3931
4799
|
skipped: outcome.skipped.map(({ item, reason }) => ({ id: item.id, reason })),
|
|
3932
4800
|
degraded: healedDegraded,
|
|
3933
4801
|
remaining: counts.eligible,
|
|
3934
|
-
deferred: counts.deferred
|
|
4802
|
+
deferred: counts.deferred,
|
|
4803
|
+
...outcome.budgetStop ? { budgetStop: outcome.budgetStop } : {}
|
|
3935
4804
|
};
|
|
3936
4805
|
}
|
|
3937
4806
|
/** Core ontology backfill pass (locks handled by {@link runOntologyBackfill}). Package-internal orchestration hook. */
|
|
@@ -3975,7 +4844,7 @@ var MaintenanceService = class {
|
|
|
3975
4844
|
const outcome = await runBatched({
|
|
3976
4845
|
items: candidates,
|
|
3977
4846
|
buildPrompt,
|
|
3978
|
-
call: (prompts) => this.options
|
|
4847
|
+
call: (prompts) => callLlm(this.options, { operation: "ontologyBackfill", entityId, trigger: "call" }, prompts),
|
|
3979
4848
|
parse: (responseText, batch) => {
|
|
3980
4849
|
const parsed = parseJsonResponse(responseText);
|
|
3981
4850
|
return {
|
|
@@ -4506,6 +5375,12 @@ var ImportExportService = class {
|
|
|
4506
5375
|
})()
|
|
4507
5376
|
};
|
|
4508
5377
|
await this.entryRepo.upsertForImport(factObj, tx);
|
|
5378
|
+
await this.entryRepo.setTemporal(fact.id, entityId, {
|
|
5379
|
+
valid_from: fact.valid_from ?? null,
|
|
5380
|
+
valid_to: fact.valid_to ?? null,
|
|
5381
|
+
superseded_by: fact.superseded_by ?? null,
|
|
5382
|
+
superseded_at: fact.superseded_at ?? null
|
|
5383
|
+
}, tx, { outbox: false });
|
|
4509
5384
|
if (blobData != null) {
|
|
4510
5385
|
factsWithPreservedBlob.set(fact.id, blobData);
|
|
4511
5386
|
if (!fact.deleted_at) preservedBlobDims.add(blobData.byteLength / 4);
|
|
@@ -4584,7 +5459,8 @@ var ImportExportService = class {
|
|
|
4584
5459
|
event_type: event.event_type,
|
|
4585
5460
|
summary: event.summary,
|
|
4586
5461
|
related_entry_id: event.related_entry_id ?? null,
|
|
4587
|
-
created_at: event.created_at
|
|
5462
|
+
created_at: event.created_at,
|
|
5463
|
+
occurred_at: event.occurred_at ?? null
|
|
4588
5464
|
},
|
|
4589
5465
|
tx
|
|
4590
5466
|
);
|
|
@@ -4831,6 +5707,21 @@ var EmbeddingService = class {
|
|
|
4831
5707
|
}
|
|
4832
5708
|
}
|
|
4833
5709
|
async tryEmbedFact(fact, ctx) {
|
|
5710
|
+
const r = await this.embedTextForFact(fact, ctx);
|
|
5711
|
+
return r.ok ? this.storeFactVector(fact, r.vector, ctx) : r;
|
|
5712
|
+
}
|
|
5713
|
+
/**
|
|
5714
|
+
* Compute half: build the embed text, call embed(), validate the result,
|
|
5715
|
+
* mark any embed-time failure, and return the validated Float32 vector.
|
|
5716
|
+
*
|
|
5717
|
+
* Failure kinds: `no_provider`, `invalid_vector`, `float32_overflow`,
|
|
5718
|
+
* `provider_error`. All embed-time failures are marked via `markFailure`;
|
|
5719
|
+
* `no_provider` deliberately does not mark (spec §2.4).
|
|
5720
|
+
*
|
|
5721
|
+
* Storage errors are NOT possible here — that is `storeFactVector`'s domain.
|
|
5722
|
+
*/
|
|
5723
|
+
async embedTextForFact(fact, ctx, opts) {
|
|
5724
|
+
const markFailures = opts?.markFailures !== false;
|
|
4834
5725
|
if (typeof this.options.llmProvider.embed !== "function") return { ok: false, kind: "no_provider" };
|
|
4835
5726
|
let tagsStr;
|
|
4836
5727
|
if (Array.isArray(fact.tags)) {
|
|
@@ -4851,8 +5742,8 @@ var EmbeddingService = class {
|
|
|
4851
5742
|
const vector = await this.options.llmProvider.embed(text);
|
|
4852
5743
|
if (vector.length === 0 || !vector.every((v) => typeof v === "number" && isFinite(v))) {
|
|
4853
5744
|
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");
|
|
5745
|
+
if (markFailures) this.reportEmbed(ctx, fact, "embedding_failed", "invalid_vector");
|
|
5746
|
+
if (markFailures) await this.markFailure(fact.id, "invalid_vector");
|
|
4856
5747
|
return { ok: false, kind: "invalid_vector" };
|
|
4857
5748
|
}
|
|
4858
5749
|
float32Vector = new Float32Array(vector);
|
|
@@ -4865,16 +5756,24 @@ var EmbeddingService = class {
|
|
|
4865
5756
|
}
|
|
4866
5757
|
if (hasNonFinite) {
|
|
4867
5758
|
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");
|
|
5759
|
+
if (markFailures) this.reportEmbed(ctx, fact, "embedding_failed", "float32_overflow");
|
|
5760
|
+
if (markFailures) await this.markFailure(fact.id, "float32_overflow");
|
|
4870
5761
|
return { ok: false, kind: "float32_overflow" };
|
|
4871
5762
|
}
|
|
4872
5763
|
} catch (err) {
|
|
4873
5764
|
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");
|
|
5765
|
+
if (markFailures) this.reportEmbed(ctx, fact, "embedding_failed", "embed_threw");
|
|
5766
|
+
if (markFailures) await this.markFailure(fact.id, "provider_error");
|
|
4876
5767
|
return { ok: false, kind: "provider_error" };
|
|
4877
5768
|
}
|
|
5769
|
+
return { ok: true, vector: float32Vector };
|
|
5770
|
+
}
|
|
5771
|
+
/**
|
|
5772
|
+
* Storage half: persist the computed vector to the entry row, then fire the
|
|
5773
|
+
* `onEmbeddingPersisted` hook. Failure kinds: `storage_error` only — embed
|
|
5774
|
+
* failures do not originate here, so they cannot be marked as such (spec §3.2, D3).
|
|
5775
|
+
*/
|
|
5776
|
+
async storeFactVector(fact, float32Vector, ctx) {
|
|
4878
5777
|
try {
|
|
4879
5778
|
await this.storeEmbeddingDimension(float32Vector.length);
|
|
4880
5779
|
const blob = new Uint8Array(float32Vector.buffer);
|
|
@@ -5059,6 +5958,40 @@ function selectWithFloors(sortedRows, floors, maxResults) {
|
|
|
5059
5958
|
return Array.from(selected).sort((a, b) => a - b).map((i) => sortedRows[i]);
|
|
5060
5959
|
}
|
|
5061
5960
|
|
|
5961
|
+
// src/utils/budget.ts
|
|
5962
|
+
function factTokenCost(f) {
|
|
5963
|
+
return estimateTokens(`${f.title} ${f.body} ${f.tags.join(" ")}`);
|
|
5964
|
+
}
|
|
5965
|
+
function packFactsByBudget(items, budget, scores) {
|
|
5966
|
+
const candidates = items.length;
|
|
5967
|
+
if (candidates === 0) return { items: [], tokensUsed: 0, candidates: 0, packed: 0 };
|
|
5968
|
+
let top = items[0];
|
|
5969
|
+
let topCost = factTokenCost(top);
|
|
5970
|
+
if (topCost > budget) {
|
|
5971
|
+
const overhead = estimateTokens(`${top.title} ${top.tags.join(" ")}`);
|
|
5972
|
+
top = { ...top, body: safeSlice(top.body, 0, Math.max(0, (budget - overhead) * 4)) };
|
|
5973
|
+
topCost = factTokenCost(top);
|
|
5974
|
+
}
|
|
5975
|
+
const kept = [{ rank: 0, item: top }];
|
|
5976
|
+
let used = topCost;
|
|
5977
|
+
const rest = items.slice(1).map((item, i) => {
|
|
5978
|
+
const rank = i + 1;
|
|
5979
|
+
const cost = factTokenCost(item);
|
|
5980
|
+
const s = scores?.get(item.id);
|
|
5981
|
+
const value = typeof s === "number" && Number.isFinite(s) && s > 0 ? s : 1 / (rank + 1);
|
|
5982
|
+
return { rank, item, cost, density: value / Math.max(1, cost) };
|
|
5983
|
+
});
|
|
5984
|
+
rest.sort((a, b) => b.density - a.density || a.rank - b.rank);
|
|
5985
|
+
for (const r of rest) {
|
|
5986
|
+
if (used + r.cost <= budget) {
|
|
5987
|
+
kept.push({ rank: r.rank, item: r.item });
|
|
5988
|
+
used += r.cost;
|
|
5989
|
+
}
|
|
5990
|
+
}
|
|
5991
|
+
kept.sort((a, b) => a.rank - b.rank);
|
|
5992
|
+
return { items: kept.map((k) => k.item), tokensUsed: used, candidates, packed: kept.length };
|
|
5993
|
+
}
|
|
5994
|
+
|
|
5062
5995
|
// src/services/RetrievalService.ts
|
|
5063
5996
|
var EMPTY_ID_SET = /* @__PURE__ */ new Set();
|
|
5064
5997
|
var RetrievalService = class {
|
|
@@ -5094,6 +6027,28 @@ var RetrievalService = class {
|
|
|
5094
6027
|
const rawMaxResults = options?.maxResults ?? config?.maxResults ?? config?.maxFtsResults ?? 10;
|
|
5095
6028
|
const maxResults = Number.isFinite(rawMaxResults) ? Math.max(0, Math.trunc(rawMaxResults)) : 10;
|
|
5096
6029
|
const excludeDrafts = (options?.excludeDrafts ?? config?.excludeDrafts ?? false) === true;
|
|
6030
|
+
const rawAsOf = options?.asOf;
|
|
6031
|
+
if (rawAsOf !== void 0 && !(typeof rawAsOf === "number" && Number.isFinite(rawAsOf) && rawAsOf >= 0)) {
|
|
6032
|
+
throw new WikiInvalidReadOptions("asOf", "must be a finite epoch-ms number >= 0");
|
|
6033
|
+
}
|
|
6034
|
+
const tokenBudget = options?.tokenBudget;
|
|
6035
|
+
if (tokenBudget !== void 0 && !(typeof tokenBudget === "number" && Number.isFinite(tokenBudget) && tokenBudget >= 0)) {
|
|
6036
|
+
throw new WikiInvalidReadOptions("tokenBudget", "must be a finite number >= 0");
|
|
6037
|
+
}
|
|
6038
|
+
const applyBudget = (input) => {
|
|
6039
|
+
if (tokenBudget === void 0) return input;
|
|
6040
|
+
const packed = packFactsByBudget(input, Math.trunc(tokenBudget), scoreByFactId);
|
|
6041
|
+
emitDiagnostic(this.options, {
|
|
6042
|
+
code: "read_budget",
|
|
6043
|
+
operation: "read",
|
|
6044
|
+
trigger: "call",
|
|
6045
|
+
entityId: entityIds.length === 1 ? entityIds[0] : entityIds.join(","),
|
|
6046
|
+
detail: { candidates: packed.candidates, packed: packed.packed, tokensUsed: packed.tokensUsed }
|
|
6047
|
+
});
|
|
6048
|
+
return packed.items;
|
|
6049
|
+
};
|
|
6050
|
+
const liveMode = rawAsOf === void 0 ? "current" : "asOf";
|
|
6051
|
+
const liveAt = rawAsOf === void 0 ? Date.now() : Math.trunc(rawAsOf);
|
|
5097
6052
|
const trimmedQuery = query.trim();
|
|
5098
6053
|
const sanitizedTierFloors = trimmedQuery && exposeMetadata ? validateTierFloors(
|
|
5099
6054
|
entityIds,
|
|
@@ -5113,8 +6068,10 @@ var RetrievalService = class {
|
|
|
5113
6068
|
let usedEmbed = false;
|
|
5114
6069
|
const scoredEntityIds = this._filterScoredEntities(entityIds, sanitizedTierWeights, options?.includeZeroWeightEntities);
|
|
5115
6070
|
const draftIds = excludeDrafts && scoredEntityIds.length > 0 ? await this.entryRepo.findDraftIdsByEntityIds(scoredEntityIds) : EMPTY_ID_SET;
|
|
5116
|
-
const
|
|
5117
|
-
const
|
|
6071
|
+
const nonLiveIds = scoredEntityIds.length > 0 ? await this.entryRepo.findNonLiveIdsByEntityIds(scoredEntityIds, liveMode, liveAt) : EMPTY_ID_SET;
|
|
6072
|
+
const excludedIds = draftIds.size === 0 ? nonLiveIds : nonLiveIds.size === 0 ? draftIds : /* @__PURE__ */ new Set([...draftIds, ...nonLiveIds]);
|
|
6073
|
+
const excludedPad = excludedIds.size;
|
|
6074
|
+
const padLimit = (n) => n >= Number.MAX_SAFE_INTEGER ? n : n + excludedPad;
|
|
5118
6075
|
if (scoredEntityIds.length === 0) {
|
|
5119
6076
|
usedEmbed = true;
|
|
5120
6077
|
} else if (!skipEmbed && typeof this.options.llmProvider.embed === "function") {
|
|
@@ -5148,7 +6105,7 @@ var RetrievalService = class {
|
|
|
5148
6105
|
let miniSearchScores;
|
|
5149
6106
|
if (effectivePreFilterLimit !== void 0) {
|
|
5150
6107
|
populateCache = false;
|
|
5151
|
-
const preResults = this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, Number.MAX_SAFE_INTEGER).filter((r) => !
|
|
6108
|
+
const preResults = this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, Number.MAX_SAFE_INTEGER).filter((r) => !excludedIds.has(r.id));
|
|
5152
6109
|
if (preResults.length === 0) {
|
|
5153
6110
|
candidateRows = null;
|
|
5154
6111
|
} else {
|
|
@@ -5170,9 +6127,9 @@ var RetrievalService = class {
|
|
|
5170
6127
|
}
|
|
5171
6128
|
} else {
|
|
5172
6129
|
if (useRanker) {
|
|
5173
|
-
candidateRows = this.
|
|
6130
|
+
candidateRows = this._withoutIds(await this.entryRepo.findMetadataByEntityIds(scoredEntityIds), excludedIds);
|
|
5174
6131
|
} else {
|
|
5175
|
-
candidateRows = this.
|
|
6132
|
+
candidateRows = this._withoutIds(await this.entryRepo.findWithEmbeddingsByEntityIds(scoredEntityIds), excludedIds);
|
|
5176
6133
|
}
|
|
5177
6134
|
if (weight !== void 0 && weight < 1) {
|
|
5178
6135
|
miniSearchScores = this.searchService.getMiniSearchScores(trimmedQuery, scoredEntityIds);
|
|
@@ -5457,7 +6414,7 @@ var RetrievalService = class {
|
|
|
5457
6414
|
// read() re-sorts after applying tier weights
|
|
5458
6415
|
});
|
|
5459
6416
|
}
|
|
5460
|
-
if (
|
|
6417
|
+
if (excludedPad > 0) scored = scored.filter((s) => !excludedIds.has(s.id));
|
|
5461
6418
|
if (scored.length > 0) {
|
|
5462
6419
|
scored = scored.map((row) => ({
|
|
5463
6420
|
...row,
|
|
@@ -5521,7 +6478,7 @@ var RetrievalService = class {
|
|
|
5521
6478
|
if (!usedEmbed && scoredEntityIds.length > 0) {
|
|
5522
6479
|
const hasActiveFloors = sanitizedTierFloors !== void 0 && Object.values(sanitizedTierFloors).some((f) => f > 0);
|
|
5523
6480
|
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) => !
|
|
6481
|
+
const results = this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, fallbackOversampledLimit).filter((r) => !excludedIds.has(r.id));
|
|
5525
6482
|
const candidates = results.map((r) => ({
|
|
5526
6483
|
id: r.id,
|
|
5527
6484
|
entity_id: r.entity_id,
|
|
@@ -5539,13 +6496,18 @@ var RetrievalService = class {
|
|
|
5539
6496
|
}
|
|
5540
6497
|
}
|
|
5541
6498
|
}
|
|
6499
|
+
facts = applyBudget(facts);
|
|
5542
6500
|
if (facts.length > 0) {
|
|
5543
6501
|
const ids = facts.map((f) => f.id);
|
|
5544
6502
|
const now = Date.now();
|
|
5545
6503
|
await this.entryRepo.trackAccess(ids, now);
|
|
5546
6504
|
}
|
|
5547
6505
|
} else {
|
|
5548
|
-
facts =
|
|
6506
|
+
facts = await this.entryRepo.findRecentByEntityIds(entityIds, maxResults, void 0, {
|
|
6507
|
+
...excludeDrafts ? { excludeDrafts: true } : {},
|
|
6508
|
+
live: { mode: liveMode, t: liveAt }
|
|
6509
|
+
});
|
|
6510
|
+
facts = applyBudget(facts);
|
|
5549
6511
|
}
|
|
5550
6512
|
const eventsLimit = Math.min(10 * entityIds.length, 100);
|
|
5551
6513
|
const [tasks, events] = await Promise.all([
|
|
@@ -5581,8 +6543,8 @@ var RetrievalService = class {
|
|
|
5581
6543
|
_tieBreakSort(items) {
|
|
5582
6544
|
items.sort((a, b) => this._compareScoredRows(a, b));
|
|
5583
6545
|
}
|
|
5584
|
-
|
|
5585
|
-
return
|
|
6546
|
+
_withoutIds(rows, excludeIds) {
|
|
6547
|
+
return excludeIds.size === 0 ? rows : rows.filter((row) => !excludeIds.has(row.id));
|
|
5586
6548
|
}
|
|
5587
6549
|
/**
|
|
5588
6550
|
* Comparator for score + deterministic tie-break fields.
|
|
@@ -5664,6 +6626,18 @@ var WriteService = class {
|
|
|
5664
6626
|
this.metadataRepo = metadataRepo;
|
|
5665
6627
|
this.jobManager = jobManager;
|
|
5666
6628
|
this.maintenanceService = maintenanceService;
|
|
6629
|
+
this.inflight = /* @__PURE__ */ new Set();
|
|
6630
|
+
}
|
|
6631
|
+
track(p) {
|
|
6632
|
+
this.inflight.add(p);
|
|
6633
|
+
p.finally(() => this.inflight.delete(p)).catch(() => {
|
|
6634
|
+
});
|
|
6635
|
+
}
|
|
6636
|
+
/** Await every background job this instance started (spec §6.2). Loops because a finishing job can start another. */
|
|
6637
|
+
async drain() {
|
|
6638
|
+
while (this.inflight.size > 0) {
|
|
6639
|
+
await Promise.allSettled([...this.inflight]);
|
|
6640
|
+
}
|
|
5667
6641
|
}
|
|
5668
6642
|
async write(entityId, event) {
|
|
5669
6643
|
if (typeof entityId !== "string" || entityId.length === 0 || entityId.length > 200 || entityId.includes("\0")) {
|
|
@@ -5690,6 +6664,7 @@ var WriteService = class {
|
|
|
5690
6664
|
}
|
|
5691
6665
|
const id = generateId("evt_");
|
|
5692
6666
|
const now = Date.now();
|
|
6667
|
+
const occurredAt = event.occurred_at == null ? null : assertEpochMs("event.occurred_at", event.occurred_at);
|
|
5693
6668
|
let eventType = event.event_type;
|
|
5694
6669
|
if (!["observation", "decision", "action", "outcome"].includes(eventType)) {
|
|
5695
6670
|
eventType = "observation";
|
|
@@ -5700,8 +6675,14 @@ var WriteService = class {
|
|
|
5700
6675
|
event_type: eventType,
|
|
5701
6676
|
summary,
|
|
5702
6677
|
related_entry_id: relatedEntryId,
|
|
5703
|
-
created_at: now
|
|
6678
|
+
created_at: now,
|
|
6679
|
+
occurred_at: occurredAt
|
|
5704
6680
|
};
|
|
6681
|
+
const deferred = this.options.config?.maintenance === "deferred";
|
|
6682
|
+
if (deferred) {
|
|
6683
|
+
await this.eventRepo.add(newEvent);
|
|
6684
|
+
return;
|
|
6685
|
+
}
|
|
5705
6686
|
let shouldRunLibrarian = false;
|
|
5706
6687
|
let librarianCount = 0;
|
|
5707
6688
|
let prevMemoryCheckpoint = 0;
|
|
@@ -5723,27 +6704,41 @@ var WriteService = class {
|
|
|
5723
6704
|
prevMemoryCheckpoint = memoryCheckpoint;
|
|
5724
6705
|
await this.metadataRepo.updateCheckpoint(entityId, { memory: count }, tx);
|
|
5725
6706
|
}
|
|
6707
|
+
} else {
|
|
6708
|
+
const tokenThreshold = this.options.config?.autoLibrarianTokenThreshold;
|
|
6709
|
+
if (typeof tokenThreshold === "number" && tokenThreshold > 0 && !this.jobManager.isBlocked("librarian", entityId)) {
|
|
6710
|
+
const wm = await this.metadataRepo.getLibrarianWatermark(entityId, tx);
|
|
6711
|
+
const chars = await this.eventRepo.sumSummaryCharsAfter(entityId, wm, tx);
|
|
6712
|
+
if (Math.ceil(chars / 4) >= tokenThreshold) {
|
|
6713
|
+
shouldRunLibrarian = true;
|
|
6714
|
+
librarianCount = count;
|
|
6715
|
+
prevMemoryCheckpoint = memoryCheckpoint;
|
|
6716
|
+
await this.metadataRepo.updateCheckpoint(entityId, { memory: count }, tx);
|
|
6717
|
+
}
|
|
6718
|
+
}
|
|
5726
6719
|
}
|
|
5727
6720
|
});
|
|
5728
6721
|
if (shouldRunLibrarian) {
|
|
5729
6722
|
try {
|
|
5730
6723
|
this.jobManager.acquireLock("librarian", entityId);
|
|
5731
6724
|
const stage = { job: "librarian" };
|
|
5732
|
-
this.runLibrarianThenMaybeHeal(entityId, librarianCount, prevMemoryCheckpoint, stage).catch((err) => {
|
|
6725
|
+
const job = this.runLibrarianThenMaybeHeal(entityId, librarianCount, prevMemoryCheckpoint, stage).catch((err) => {
|
|
5733
6726
|
console.error(err);
|
|
5734
6727
|
this.reportBackgroundFailure(entityId, stage.job);
|
|
5735
6728
|
}).finally(() => {
|
|
5736
6729
|
this.jobManager.releaseLock("librarian", entityId);
|
|
5737
6730
|
});
|
|
6731
|
+
this.track(job);
|
|
5738
6732
|
} catch (e) {
|
|
5739
6733
|
if (!(e instanceof WikiBusyError)) throw e;
|
|
5740
6734
|
await this.metadataRepo.updateCheckpoint(entityId, { memory: prevMemoryCheckpoint }, this.db);
|
|
5741
6735
|
}
|
|
5742
6736
|
} else if (!this.jobManager.isBlocked("librarian", entityId)) {
|
|
5743
|
-
this.maybeRunHeal(entityId, eventCount).catch((err) => {
|
|
6737
|
+
const healJob = this.maybeRunHeal(entityId, eventCount).catch((err) => {
|
|
5744
6738
|
console.error(err);
|
|
5745
6739
|
this.reportBackgroundFailure(entityId, "heal");
|
|
5746
6740
|
});
|
|
6741
|
+
this.track(healJob);
|
|
5747
6742
|
}
|
|
5748
6743
|
}
|
|
5749
6744
|
async runLibrarianThenMaybeHeal(entityId, currentEventCount, prevCheckpoint, stage) {
|
|
@@ -5790,6 +6785,6 @@ var WriteService = class {
|
|
|
5790
6785
|
}
|
|
5791
6786
|
};
|
|
5792
6787
|
|
|
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-
|
|
6788
|
+
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 };
|
|
6789
|
+
//# sourceMappingURL=chunk-M76CF2Q6.mjs.map
|
|
6790
|
+
//# sourceMappingURL=chunk-M76CF2Q6.mjs.map
|