@equationalapplications/core-llm-wiki 7.2.1 → 7.4.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.
@@ -168,6 +168,14 @@ var WikiInvalidReadOptions = class _WikiInvalidReadOptions extends Error {
168
168
  Object.setPrototypeOf(this, _WikiInvalidReadOptions.prototype);
169
169
  }
170
170
  };
171
+ var WikiDraftNotFound = class _WikiDraftNotFound extends Error {
172
+ constructor() {
173
+ super("No draft fact with that id exists for this entity.");
174
+ this.code = "WIKI_DRAFT_NOT_FOUND";
175
+ this.name = "WikiDraftNotFound";
176
+ Object.setPrototypeOf(this, _WikiDraftNotFound.prototype);
177
+ }
178
+ };
171
179
 
172
180
  // src/utils/ids.ts
173
181
  var _injectedGetRandomValues = null;
@@ -336,20 +344,27 @@ function mergeOntologyUpdates(current, updates) {
336
344
  function validateInlineEdges(sourceType, _targetType, edges, manifest, opts) {
337
345
  const strict = opts?.strict === true;
338
346
  const entityId = opts?.entityId ?? "";
347
+ const drops = opts?.drops;
348
+ const drop = (reason, edgeType) => {
349
+ drops?.push({ reason, sourceId: null, edgeType, sourceNodeType: sourceType, targetNodeType: null });
350
+ };
339
351
  if (!Array.isArray(edges)) {
340
352
  if (strict) throw new WikiStrictOntologyViolation(entityId, "edge", "");
353
+ drop("invalid_shape", null);
341
354
  return [];
342
355
  }
343
356
  const valid = [];
344
357
  for (const edge of edges) {
345
358
  if (typeof edge?.edge_type !== "string" || typeof edge?.target_title !== "string") {
346
359
  if (strict) throw new WikiStrictOntologyViolation(entityId, "edge", String(edge?.edge_type ?? ""));
360
+ drop("invalid_shape", typeof edge?.edge_type === "string" ? edge.edge_type : null);
347
361
  continue;
348
362
  }
349
363
  const defs = resolveEdgeDefinitions(edge.edge_type, manifest);
350
364
  const match = defs.find((d) => typeSatisfies(d.source_type, sourceType, manifest));
351
365
  if (!match) {
352
366
  if (strict) throw new WikiStrictOntologyViolation(entityId, "edge", edge.edge_type);
367
+ drop("type_not_in_manifest", edge.edge_type);
353
368
  continue;
354
369
  }
355
370
  valid.push({ edge_type: match.type, target_title: edge.target_title });
@@ -1526,6 +1541,17 @@ function validateTask(task) {
1526
1541
  priority
1527
1542
  };
1528
1543
  }
1544
+ function factRejectionReason(raw) {
1545
+ const fact = raw;
1546
+ if (typeof fact?.title !== "string" || typeof fact?.body !== "string") return "invalid_shape";
1547
+ if (!clip(fact.title, 80)) return "missing_title";
1548
+ return "missing_body";
1549
+ }
1550
+ function taskRejectionReason(raw) {
1551
+ const task = raw;
1552
+ if (typeof task?.description !== "string") return "invalid_shape";
1553
+ return "missing_description";
1554
+ }
1529
1555
  function normalizeSourceRef(value) {
1530
1556
  if (typeof value !== "string") return null;
1531
1557
  const cleaned = value.replace(/[^A-Za-z0-9._\- ]/g, "").trim().slice(0, 255);
@@ -1765,6 +1791,104 @@ function applyBodyTruncation(candidates, attemptLevel, bodyTruncationChars) {
1765
1791
  var DEFAULT_MAX_CHUNK_LENGTH = 12e3;
1766
1792
  var DEFAULT_CHUNK_OVERLAP = 400;
1767
1793
 
1794
+ // src/utils/diagnostics.ts
1795
+ var SEVERITY = {
1796
+ ingest_chunk_failed: "warn",
1797
+ fact_rejected: "warn",
1798
+ task_rejected: "warn",
1799
+ fact_deduplicated: "info",
1800
+ edge_dropped: "warn",
1801
+ embedding_failed: "warn",
1802
+ hook_failed: "warn",
1803
+ background_job_failed: "error",
1804
+ heal_skipped: "warn",
1805
+ grounding_missing: "warn",
1806
+ grounding_failed: "warn",
1807
+ classification_low_confidence: "info",
1808
+ classification_invalid: "warn"
1809
+ };
1810
+ var MESSAGE = {
1811
+ ingest_chunk_failed: "One or more document chunks failed to parse or generate.",
1812
+ fact_rejected: "An extracted fact failed validation and was not written.",
1813
+ task_rejected: "An extracted task failed validation and was not written.",
1814
+ fact_deduplicated: "An extracted fact duplicated an existing fact and was not written.",
1815
+ edge_dropped: "An extracted edge could not be resolved against the ontology and was not written.",
1816
+ embedding_failed: "A fact could not be embedded.",
1817
+ hook_failed: "A host hook threw or rejected.",
1818
+ background_job_failed: "A background maintenance job failed.",
1819
+ heal_skipped: "Heal skipped a candidate fact.",
1820
+ grounding_missing: "A fact carried no qualifying evidence and was stored as a draft.",
1821
+ grounding_failed: "A fact carried evidence not found in its source and was stored as a draft.",
1822
+ classification_low_confidence: "A classifier answer was below the confidence threshold and was not applied.",
1823
+ classification_invalid: "A classifier answer was invalid and was not applied."
1824
+ };
1825
+ var warnedNonFunction = /* @__PURE__ */ new WeakSet();
1826
+ function copyDetail(detail) {
1827
+ const copy = { ...detail };
1828
+ if (detail.chunkIndexes) copy.chunkIndexes = detail.chunkIndexes.slice();
1829
+ return copy;
1830
+ }
1831
+ function emitDiagnostic(options, input) {
1832
+ const hook = options.onDiagnostic;
1833
+ if (hook === void 0 || hook === null) return;
1834
+ if (typeof hook !== "function") {
1835
+ if (!warnedNonFunction.has(options)) {
1836
+ warnedNonFunction.add(options);
1837
+ console.warn("[WikiMemory] onDiagnostic is not a function; diagnostics are disabled.");
1838
+ }
1839
+ return;
1840
+ }
1841
+ const diagnostic = {
1842
+ code: input.code,
1843
+ severity: SEVERITY[input.code],
1844
+ operation: input.operation,
1845
+ trigger: input.trigger,
1846
+ entityId: input.entityId,
1847
+ at: Date.now(),
1848
+ message: MESSAGE[input.code],
1849
+ ...input.detail ? { detail: copyDetail(input.detail) } : {}
1850
+ };
1851
+ try {
1852
+ const returned = hook(diagnostic);
1853
+ if (returned !== void 0) {
1854
+ Promise.resolve(returned).catch((err) => {
1855
+ console.warn("[WikiMemory] onDiagnostic hook rejected:", err);
1856
+ });
1857
+ }
1858
+ } catch (err) {
1859
+ console.warn("[WikiMemory] onDiagnostic hook threw:", err);
1860
+ }
1861
+ }
1862
+ var DiagnosticBuffer = class {
1863
+ constructor() {
1864
+ this.items = [];
1865
+ }
1866
+ push(input) {
1867
+ this.items.push(input);
1868
+ }
1869
+ get size() {
1870
+ return this.items.length;
1871
+ }
1872
+ flush(options) {
1873
+ const pending = this.items;
1874
+ this.items = [];
1875
+ for (const input of pending) emitDiagnostic(options, input);
1876
+ }
1877
+ discard() {
1878
+ this.items = [];
1879
+ }
1880
+ };
1881
+ function edgeDropDiagnostic(drop, ctx) {
1882
+ const detail = { reason: drop.reason };
1883
+ const factId = ctx.factId ?? drop.sourceId;
1884
+ if (factId) detail.factId = factId;
1885
+ if (drop.edgeType) detail.edgeType = drop.edgeType;
1886
+ if (drop.sourceNodeType) detail.sourceNodeType = drop.sourceNodeType;
1887
+ if (drop.targetNodeType) detail.targetNodeType = drop.targetNodeType;
1888
+ if (ctx.sourceRef) detail.sourceRef = ctx.sourceRef;
1889
+ return { code: "edge_dropped", operation: ctx.operation, trigger: ctx.trigger, entityId: ctx.entityId, detail };
1890
+ }
1891
+
1768
1892
  // src/services/IngestionService.ts
1769
1893
  function zeroChunkResult(duplicateOf) {
1770
1894
  const result = {
@@ -1832,9 +1956,24 @@ var IngestionService = class {
1832
1956
  try {
1833
1957
  const responseText = await this.options.llmProvider.generateText({ systemPrompt, userPrompt });
1834
1958
  const result2 = parseJsonResponse(responseText);
1959
+ const rawFacts = Array.isArray(result2.facts) ? result2.facts : [];
1960
+ const facts = [];
1961
+ const itemIndexes = [];
1962
+ const rejected = [];
1963
+ rawFacts.forEach((raw, itemIndex) => {
1964
+ const valid = validateFact(raw);
1965
+ if (valid) {
1966
+ facts.push(valid);
1967
+ itemIndexes.push(itemIndex);
1968
+ } else {
1969
+ rejected.push({ itemIndex, reason: factRejectionReason(raw) });
1970
+ }
1971
+ });
1835
1972
  return {
1836
1973
  status: "ok",
1837
- facts: (Array.isArray(result2.facts) ? result2.facts : []).map(validateFact).filter((f) => f !== null),
1974
+ facts,
1975
+ itemIndexes,
1976
+ rejected,
1838
1977
  ontology_updates: result2.ontology_updates
1839
1978
  };
1840
1979
  } catch (e) {
@@ -1879,23 +2018,48 @@ var IngestionService = class {
1879
2018
  const failures = [];
1880
2019
  const seen = /* @__PURE__ */ new Set();
1881
2020
  const orderedChunkFacts = [];
1882
- for (const slot of chunkResults) {
2021
+ const diagBuffer = new DiagnosticBuffer();
2022
+ const diagBase = { entityId, operation: "ingest", trigger: "call" };
2023
+ for (const [chunkIndex, slot] of chunkResults.entries()) {
1883
2024
  if (slot.status === "failed") {
1884
2025
  failedChunks++;
1885
2026
  failures.push(slot.error);
1886
2027
  continue;
1887
2028
  }
1888
2029
  ingestedChunks++;
2030
+ for (const r of slot.rejected) {
2031
+ diagBuffer.push({ ...diagBase, code: "fact_rejected", detail: { sourceRef, chunkIndex, itemIndex: r.itemIndex, reason: r.reason } });
2032
+ }
1889
2033
  const dedupedFacts = [];
1890
- for (const fact of slot.facts) {
2034
+ slot.facts.forEach((fact, k) => {
1891
2035
  const normalizedTitle = normalizeTitleKey(fact.title);
1892
2036
  if (!seen.has(normalizedTitle)) {
1893
2037
  seen.add(normalizedTitle);
1894
2038
  dedupedFacts.push(fact);
2039
+ } else {
2040
+ diagBuffer.push({
2041
+ ...diagBase,
2042
+ code: "fact_deduplicated",
2043
+ detail: { sourceRef, chunkIndex, itemIndex: slot.itemIndexes[k], reason: "exact_title" }
2044
+ });
1895
2045
  }
1896
- }
2046
+ });
1897
2047
  orderedChunkFacts.push({ facts: dedupedFacts, ontology_updates: slot.ontology_updates });
1898
2048
  }
2049
+ for (const reason of ["parse", "llm"]) {
2050
+ const ofReason = failures.filter((f) => f.source === reason);
2051
+ if (ofReason.length === 0) continue;
2052
+ diagBuffer.push({
2053
+ ...diagBase,
2054
+ code: "ingest_chunk_failed",
2055
+ detail: {
2056
+ sourceRef,
2057
+ reason,
2058
+ count: ofReason.length,
2059
+ chunkIndexes: ofReason.map((f) => f.chunkIndex).sort((a, b) => a - b).slice(0, 20)
2060
+ }
2061
+ });
2062
+ }
1899
2063
  if (failedChunks === chunks.length) {
1900
2064
  throw new WikiIngestEmptyError({
1901
2065
  parseFailures: failures,
@@ -1908,7 +2072,7 @@ var IngestionService = class {
1908
2072
  try {
1909
2073
  if (failedChunks === 0) {
1910
2074
  const fullResult = await this.db.withTransactionAsync(async (tx) => {
1911
- return await this.runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx);
2075
+ return await this.runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx, diagBuffer);
1912
2076
  });
1913
2077
  deletedSourceFactIds.push(...fullResult.deletedSourceFactIds);
1914
2078
  insertedFacts.push(...fullResult.insertedFacts);
@@ -1916,7 +2080,7 @@ var IngestionService = class {
1916
2080
  const partialResult = await this.db.withTransactionAsync(async (tx) => {
1917
2081
  const flat = [];
1918
2082
  for (const slot of orderedChunkFacts) flat.push(...slot.facts);
1919
- return await this.appendPartialFacts(entityId, sourceRef, flat, tx);
2083
+ return await this.appendPartialFacts(entityId, sourceRef, flat, tx, diagBuffer);
1920
2084
  });
1921
2085
  insertedFacts.push(...partialResult.insertedDescriptors);
1922
2086
  }
@@ -1930,6 +2094,7 @@ var IngestionService = class {
1930
2094
  }
1931
2095
  return zeroChunkResult(canonical);
1932
2096
  }
2097
+ diagBuffer.flush(this.options);
1933
2098
  await this.searchService.sync(entityId);
1934
2099
  if (failedChunks === 0) {
1935
2100
  const uniqueDeletedSourceFactIds = Array.from(new Set(deletedSourceFactIds));
@@ -1938,11 +2103,18 @@ var IngestionService = class {
1938
2103
  await this.embeddingService.notifyEmbeddingPersisted(entityId, factId, null);
1939
2104
  } catch (hookErr) {
1940
2105
  console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during ingest for ${factId}:`, hookErr);
2106
+ emitDiagnostic(this.options, {
2107
+ entityId,
2108
+ operation: "ingest",
2109
+ trigger: "call",
2110
+ code: "hook_failed",
2111
+ detail: { factId, reason: "on_embedding_persisted" }
2112
+ });
1941
2113
  }
1942
2114
  }
1943
2115
  }
1944
2116
  for (const fact of insertedFacts) {
1945
- await this.embeddingService.embedFact(fact);
2117
+ await this.embeddingService.embedFact(fact, { operation: "ingest", trigger: "call" });
1946
2118
  }
1947
2119
  this.searchService.evictCache(entityId);
1948
2120
  const result = {
@@ -2088,6 +2260,19 @@ var IngestionService = class {
2088
2260
  const match = candidates[0];
2089
2261
  if (!match) {
2090
2262
  if (strictEffective) throw new WikiStrictOntologyViolation(entityId, "edge", edge.type);
2263
+ opts?.diag?.buffer.push({
2264
+ entityId,
2265
+ operation: opts.diag.operation,
2266
+ trigger: "call",
2267
+ code: "edge_dropped",
2268
+ detail: {
2269
+ reason: "manifest_violation",
2270
+ factId: edge.sourceId,
2271
+ edgeType: edge.type,
2272
+ ...sourceType ? { sourceNodeType: sourceType } : {},
2273
+ sourceRef: params.sourceRef
2274
+ }
2275
+ });
2091
2276
  continue;
2092
2277
  }
2093
2278
  validEdges.push({
@@ -2128,7 +2313,7 @@ var IngestionService = class {
2128
2313
  * On the partial path the caller never invokes this method; the empty
2129
2314
  * arrays stay empty.
2130
2315
  */
2131
- async runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx) {
2316
+ async runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx, diagBuffer) {
2132
2317
  const deletedSourceFactIds = [];
2133
2318
  const insertedFacts = [];
2134
2319
  deletedSourceFactIds.push(...await this.entryRepo.findIdsBySource(entityId, sourceRef, null, tx, false));
@@ -2153,8 +2338,12 @@ var IngestionService = class {
2153
2338
  }
2154
2339
  for (const fact of facts) {
2155
2340
  const ontologyFact = fact;
2156
- const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false }) ?? { okf_type: null, edges: [] };
2157
2341
  const id = generateId("fact_");
2342
+ const validationDrops = [];
2343
+ const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false, drops: validationDrops }) ?? { okf_type: null, edges: [] };
2344
+ for (const drop of validationDrops) {
2345
+ diagBuffer.push(edgeDropDiagnostic(drop, { entityId, operation: "ingest", trigger: "call", sourceRef, factId: id }));
2346
+ }
2158
2347
  hostNodes.push({
2159
2348
  id,
2160
2349
  type: ontologyFact.okf_type ?? "",
@@ -2179,6 +2368,7 @@ var IngestionService = class {
2179
2368
  }
2180
2369
  const hostEdges = [];
2181
2370
  for (const req of rawEdgeRequests) {
2371
+ const resolveDrops = [];
2182
2372
  const resolved = this.ontologyService?.resolveEdges(
2183
2373
  entityId,
2184
2374
  req.sourceId,
@@ -2186,8 +2376,12 @@ var IngestionService = class {
2186
2376
  req.edges,
2187
2377
  manifest,
2188
2378
  titleIndex,
2189
- now
2379
+ now,
2380
+ resolveDrops
2190
2381
  ) ?? [];
2382
+ for (const drop of resolveDrops) {
2383
+ diagBuffer.push(edgeDropDiagnostic(drop, { entityId, operation: "ingest", trigger: "call", sourceRef }));
2384
+ }
2191
2385
  for (const e of resolved) {
2192
2386
  hostEdges.push({ type: e.edge_type, sourceId: e.source_id, targetId: e.target_id });
2193
2387
  }
@@ -2196,7 +2390,7 @@ var IngestionService = class {
2196
2390
  entityId,
2197
2391
  { sourceRef, sourceHash, nodes: hostNodes, edges: hostEdges },
2198
2392
  tx,
2199
- { strict: false }
2393
+ { strict: false, diag: { buffer: diagBuffer, operation: "ingest" } }
2200
2394
  );
2201
2395
  return { deletedSourceFactIds, insertedFacts };
2202
2396
  }
@@ -2220,7 +2414,7 @@ var IngestionService = class {
2220
2414
  * Runs INSIDE the caller's `tx`. Does not open a nested transaction.
2221
2415
  * Returns `{ inserted, skippedDuplicate }` for observability.
2222
2416
  */
2223
- async appendPartialFacts(entityId, sourceRef, dedupedFacts, tx) {
2417
+ async appendPartialFacts(entityId, sourceRef, dedupedFacts, tx, diagBuffer) {
2224
2418
  const liveIds = await this.entryRepo.findIdsBySource(entityId, sourceRef, null, tx, false);
2225
2419
  const liveFacts = liveIds.length === 0 ? [] : await this.entryRepo.findByIds(liveIds, void 0, tx);
2226
2420
  const liveTitles = new Set(liveFacts.map((f) => normalizeTitleKey(f.title)));
@@ -2231,6 +2425,13 @@ var IngestionService = class {
2231
2425
  for (const fact of dedupedFacts) {
2232
2426
  const normalizedTitle = normalizeTitleKey(fact.title);
2233
2427
  if (liveTitles.has(normalizedTitle)) {
2428
+ diagBuffer.push({
2429
+ entityId,
2430
+ operation: "ingest",
2431
+ trigger: "call",
2432
+ code: "fact_deduplicated",
2433
+ detail: { sourceRef, reason: "exact_title" }
2434
+ });
2234
2435
  skippedDuplicate++;
2235
2436
  continue;
2236
2437
  }
@@ -2817,7 +3018,7 @@ var MaintenanceService = class {
2817
3018
  permanentlyFailed++;
2818
3019
  continue;
2819
3020
  }
2820
- const result = await this.embeddingService.tryEmbedFact(row);
3021
+ const result = await this.embeddingService.tryEmbedFact(row, { operation: "reembed", trigger: "call" });
2821
3022
  if (result.ok) embedded++;
2822
3023
  else failed++;
2823
3024
  }
@@ -2938,7 +3139,7 @@ var MaintenanceService = class {
2938
3139
  return { deleted: { entries, tasks: 0 } };
2939
3140
  }
2940
3141
  /** Core librarian pass (locks handled by {@link runLibrarian}). Package-internal orchestration hook. */
2941
- async doRunLibrarian(entityId, promptOverride) {
3142
+ async doRunLibrarian(entityId, promptOverride, trigger = "call") {
2942
3143
  const events = await this.eventRepo.getRecent(entityId, 50);
2943
3144
  const currentFactsRows = await this.entryRepo.findRecentByEntityId(entityId, 100);
2944
3145
  const currentFacts = currentFactsRows.map((f) => {
@@ -2960,8 +3161,25 @@ var MaintenanceService = class {
2960
3161
  const facts = Array.isArray(result.facts) ? result.facts : [];
2961
3162
  const tasks = Array.isArray(result.tasks) ? result.tasks : [];
2962
3163
  const ontologyUpdates = result.ontology_updates;
2963
- const validFacts = facts.map(validateFact).filter((f) => f !== null);
2964
- const validTasks = tasks.map(validateTask).filter((t) => t !== null);
3164
+ const diagBuffer = new DiagnosticBuffer();
3165
+ const diagBase = { entityId, operation: "librarian", trigger };
3166
+ const validFacts = [];
3167
+ const validFactItemIndexes = [];
3168
+ facts.forEach((raw, itemIndex) => {
3169
+ const valid = validateFact(raw);
3170
+ if (valid) {
3171
+ validFacts.push(valid);
3172
+ validFactItemIndexes.push(itemIndex);
3173
+ } else {
3174
+ diagBuffer.push({ ...diagBase, code: "fact_rejected", detail: { itemIndex, reason: factRejectionReason(raw) } });
3175
+ }
3176
+ });
3177
+ const validTasks = [];
3178
+ tasks.forEach((raw, itemIndex) => {
3179
+ const valid = validateTask(raw);
3180
+ if (valid) validTasks.push(valid);
3181
+ else diagBuffer.push({ ...diagBase, code: "task_rejected", detail: { itemIndex, reason: taskRejectionReason(raw) } });
3182
+ });
2965
3183
  const now = Date.now();
2966
3184
  const insertedFacts = [];
2967
3185
  await this.db.withTransactionAsync(async (tx) => {
@@ -2978,7 +3196,7 @@ var MaintenanceService = class {
2978
3196
  }
2979
3197
  const factsForDedupe = await this.entryRepo.findRecentByEntityId(entityId, 100, tx);
2980
3198
  const pendingEdges = [];
2981
- for (const fact of validFacts) {
3199
+ for (const [k, fact] of validFacts.entries()) {
2982
3200
  const newTokens = titleTokens(fact.title);
2983
3201
  let skip = false;
2984
3202
  if (newTokens.size >= MIN_TOKENS_TO_QUALIFY) {
@@ -2993,10 +3211,15 @@ var MaintenanceService = class {
2993
3211
  }
2994
3212
  }
2995
3213
  }
2996
- if (skip) continue;
3214
+ if (skip) {
3215
+ diagBuffer.push({ ...diagBase, code: "fact_deduplicated", detail: { itemIndex: validFactItemIndexes[k], reason: "fuzzy_title" } });
3216
+ continue;
3217
+ }
2997
3218
  const ontologyFact = fact;
2998
- const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false }) ?? { okf_type: null, edges: [] };
2999
3219
  const id = generateId("fact_");
3220
+ const validationDrops = [];
3221
+ const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false, drops: validationDrops }) ?? { okf_type: null, edges: [] };
3222
+ for (const drop of validationDrops) diagBuffer.push(edgeDropDiagnostic(drop, { ...diagBase, factId: id }));
3000
3223
  const factObj = {
3001
3224
  id,
3002
3225
  entity_id: entityId,
@@ -3023,6 +3246,7 @@ var MaintenanceService = class {
3023
3246
  }
3024
3247
  }
3025
3248
  for (const item of pendingEdges) {
3249
+ const resolveDrops = [];
3026
3250
  await this.ontologyService?.resolveAndPersistEdges(
3027
3251
  entityId,
3028
3252
  item.sourceId,
@@ -3031,8 +3255,10 @@ var MaintenanceService = class {
3031
3255
  manifest,
3032
3256
  titleIndex,
3033
3257
  tx,
3034
- now
3258
+ now,
3259
+ resolveDrops
3035
3260
  );
3261
+ for (const drop of resolveDrops) diagBuffer.push(edgeDropDiagnostic(drop, diagBase));
3036
3262
  }
3037
3263
  for (const task of validTasks) {
3038
3264
  const id = generateId("task_");
@@ -3050,9 +3276,10 @@ var MaintenanceService = class {
3050
3276
  await this.taskRepo.upsert(taskObj, tx);
3051
3277
  }
3052
3278
  });
3279
+ diagBuffer.flush(this.options);
3053
3280
  await this.searchService.sync(entityId);
3054
3281
  for (const fact of insertedFacts) {
3055
- await this.embeddingService.embedFact(fact);
3282
+ await this.embeddingService.embedFact(fact, { operation: "librarian", trigger });
3056
3283
  }
3057
3284
  this.searchService.evictCache(entityId);
3058
3285
  }
@@ -3066,6 +3293,8 @@ var MaintenanceService = class {
3066
3293
  async doRunHeal(entityId, options) {
3067
3294
  const promptOverride = options?.promptOverride;
3068
3295
  const batchSize = options?.batchSize ?? HEAL_BATCH_SIZE;
3296
+ const trigger = options?.trigger ?? "call";
3297
+ const diagBase = { entityId, operation: "heal", trigger };
3069
3298
  if (!Number.isInteger(batchSize) || batchSize < 1) {
3070
3299
  throw new Error("Invalid batchSize: must be an integer >= 1");
3071
3300
  }
@@ -3101,6 +3330,7 @@ var MaintenanceService = class {
3101
3330
  await this.embeddingService.notifyEmbeddingPersisted(entityId, factId, null);
3102
3331
  } catch (hookErr) {
3103
3332
  console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during heal orphan pass for ${factId}:`, hookErr);
3333
+ emitDiagnostic(this.options, { ...diagBase, code: "hook_failed", detail: { factId, reason: "on_embedding_persisted" } });
3104
3334
  }
3105
3335
  }
3106
3336
  const healCandidates = await this.entryRepo.findHealCandidatesByEntityId(entityId, batchSize, recheckCutoff);
@@ -3173,22 +3403,38 @@ var MaintenanceService = class {
3173
3403
  const safeDowngradedSet = /* @__PURE__ */ new Set();
3174
3404
  const safeDeletedSet = /* @__PURE__ */ new Set();
3175
3405
  const newFacts = [];
3406
+ const newFactItemIndexes = [];
3176
3407
  for (const batchResult of outcome.results) {
3177
3408
  const mutableIds = new Set(batchResult.batch.map((f) => f.id));
3178
3409
  for (const id of batchResult.downgraded) if (mutableIds.has(id)) safeDowngradedSet.add(id);
3179
3410
  for (const id of batchResult.deleted) if (mutableIds.has(id)) safeDeletedSet.add(id);
3180
- newFacts.push(...batchResult.newFacts);
3411
+ batchResult.newFacts.forEach((raw, itemIndex) => {
3412
+ newFacts.push(raw);
3413
+ newFactItemIndexes.push(itemIndex);
3414
+ });
3181
3415
  }
3182
3416
  const safeDowngraded = Array.from(safeDowngradedSet);
3183
3417
  const safeDeleted = Array.from(safeDeletedSet);
3184
- const validNewFacts = newFacts.map(validateFact).filter((f) => f !== null);
3418
+ const diagBuffer = new DiagnosticBuffer();
3419
+ const validNewFacts = [];
3420
+ const validNewFactItemIndexes = [];
3421
+ newFacts.forEach((raw, k) => {
3422
+ const itemIndex = newFactItemIndexes[k];
3423
+ const valid = validateFact(raw);
3424
+ if (valid) {
3425
+ validNewFacts.push(valid);
3426
+ validNewFactItemIndexes.push(itemIndex);
3427
+ } else {
3428
+ diagBuffer.push({ ...diagBase, code: "fact_rejected", detail: { itemIndex, reason: factRejectionReason(raw) } });
3429
+ }
3430
+ });
3185
3431
  const insertedFacts = [];
3186
3432
  const uniqueDeletedFactIds = Array.from(new Set(safeDeleted));
3187
3433
  await this.db.withTransactionAsync(async (tx) => {
3188
3434
  await this.entryRepo.downgradeByIds(safeDowngraded, entityId, tx);
3189
3435
  await this.entryRepo.softDeleteByIds(safeDeleted, entityId, tx);
3190
3436
  const healFactsForDedupe = await this.entryRepo.findInferredTitlesByEntityId(entityId, tx);
3191
- for (const fact of validNewFacts) {
3437
+ for (const [k, fact] of validNewFacts.entries()) {
3192
3438
  const newTokens = titleTokens(fact.title);
3193
3439
  let skip = false;
3194
3440
  if (newTokens.size >= MIN_TOKENS_TO_QUALIFY) {
@@ -3202,7 +3448,10 @@ var MaintenanceService = class {
3202
3448
  }
3203
3449
  }
3204
3450
  }
3205
- if (skip) continue;
3451
+ if (skip) {
3452
+ diagBuffer.push({ ...diagBase, code: "fact_deduplicated", detail: { itemIndex: validNewFactItemIndexes[k], reason: "fuzzy_title" } });
3453
+ continue;
3454
+ }
3206
3455
  const id = generateId("fact_");
3207
3456
  const factObj = {
3208
3457
  id,
@@ -3237,16 +3486,21 @@ var MaintenanceService = class {
3237
3486
  tx
3238
3487
  );
3239
3488
  });
3489
+ for (const { item, reason } of outcome.skipped) {
3490
+ diagBuffer.push({ ...diagBase, code: "heal_skipped", detail: { factId: item.id, reason } });
3491
+ }
3492
+ diagBuffer.flush(this.options);
3240
3493
  await this.searchService.sync(entityId);
3241
3494
  for (const factId of uniqueDeletedFactIds) {
3242
3495
  try {
3243
3496
  await this.embeddingService.notifyEmbeddingPersisted(entityId, factId, null);
3244
3497
  } catch (hookErr) {
3245
3498
  console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during heal for ${factId}:`, hookErr);
3499
+ emitDiagnostic(this.options, { ...diagBase, code: "hook_failed", detail: { factId, reason: "on_embedding_persisted" } });
3246
3500
  }
3247
3501
  }
3248
3502
  for (const fact of insertedFacts) {
3249
- await this.embeddingService.embedFact(fact);
3503
+ await this.embeddingService.embedFact(fact, { operation: "heal", trigger });
3250
3504
  }
3251
3505
  this.searchService.evictCache(entityId);
3252
3506
  const skippedIds = new Set(outcome.skipped.map(({ item }) => item.id));
@@ -3386,6 +3640,8 @@ var MaintenanceService = class {
3386
3640
  let failedValidation = 0;
3387
3641
  let edgesAdded = 0;
3388
3642
  let abortedOntologyOff = false;
3643
+ const diagBuffer = new DiagnosticBuffer();
3644
+ const diagBase = { entityId, operation: "ontologyBackfill", trigger: "call" };
3389
3645
  await this.db.withTransactionAsync(async (tx) => {
3390
3646
  let { mode: txMode, manifest } = await ontologyService.getEffectiveState(entityId, tx);
3391
3647
  if (txMode === "off") {
@@ -3410,6 +3666,7 @@ var MaintenanceService = class {
3410
3666
  continue;
3411
3667
  }
3412
3668
  applied.add(fact.id);
3669
+ const validationDrops = [];
3413
3670
  const normalized = ontologyService.validateAndNormalizeFact(
3414
3671
  {
3415
3672
  title: fact.title,
@@ -3420,8 +3677,11 @@ var MaintenanceService = class {
3420
3677
  edges: classification.edges
3421
3678
  },
3422
3679
  manifest,
3423
- { strict: false }
3680
+ { strict: false, drops: validationDrops }
3424
3681
  );
3682
+ for (const drop of validationDrops) {
3683
+ diagBuffer.push(edgeDropDiagnostic(drop, { ...diagBase, factId: fact.id }));
3684
+ }
3425
3685
  if (!normalized.okf_type) {
3426
3686
  failedValidation++;
3427
3687
  continue;
@@ -3435,7 +3695,8 @@ var MaintenanceService = class {
3435
3695
  }
3436
3696
  }
3437
3697
  for (const item of pendingEdges) {
3438
- edgesAdded += await ontologyService.resolveAndPersistEdges(
3698
+ const resolveDrops = [];
3699
+ const added = await ontologyService.resolveAndPersistEdges(
3439
3700
  entityId,
3440
3701
  item.sourceId,
3441
3702
  item.sourceType,
@@ -3443,11 +3704,17 @@ var MaintenanceService = class {
3443
3704
  manifest,
3444
3705
  titleIndex,
3445
3706
  tx,
3446
- now
3707
+ now,
3708
+ resolveDrops
3447
3709
  );
3710
+ edgesAdded += added;
3711
+ for (const drop of resolveDrops) {
3712
+ diagBuffer.push(edgeDropDiagnostic(drop, diagBase));
3713
+ }
3448
3714
  }
3449
3715
  await this.entryRepo.markOntologyChecked(batch.map((f) => f.id), entityId, now, tx);
3450
3716
  });
3717
+ diagBuffer.flush(this.options);
3451
3718
  return { typed, failedValidation, edgesAdded, abortedOntologyOff };
3452
3719
  }
3453
3720
  _validatePruneDuration(value, name) {
@@ -3849,7 +4116,7 @@ var ImportExportService = class {
3849
4116
  title: clipped?.title ?? fact.title,
3850
4117
  body: clipped?.body ?? fact.body,
3851
4118
  tags: Array.isArray(fact.tags) || typeof fact.tags === "string" ? fact.tags : []
3852
- });
4119
+ }, { operation: "importDump", trigger: "call" });
3853
4120
  if (!embedded) {
3854
4121
  await this.embeddingService.notifyEmbeddingPersisted(entityId, fact.id, null);
3855
4122
  }
@@ -3874,6 +4141,11 @@ var ImportExportService = class {
3874
4141
  `[WikiMemory] onEmbeddingPersisted hook failed for preserved-blob fact ${fact.id}:`,
3875
4142
  hookErr
3876
4143
  );
4144
+ this.embeddingService.reportHookFailed(
4145
+ { operation: "importDump", trigger: "call" },
4146
+ entityId,
4147
+ fact.id
4148
+ );
3877
4149
  }
3878
4150
  }
3879
4151
  }
@@ -3890,6 +4162,11 @@ var ImportExportService = class {
3890
4162
  `[WikiMemory] onEmbeddingPersisted(vector=null) hook failed for soft-deleted fact ${factId}:`,
3891
4163
  hookErr
3892
4164
  );
4165
+ this.embeddingService.reportHookFailed(
4166
+ { operation: "importDump", trigger: "call" },
4167
+ entityId,
4168
+ factId
4169
+ );
3893
4170
  }
3894
4171
  }
3895
4172
  }
@@ -4052,7 +4329,7 @@ var EmbeddingService = class {
4052
4329
  });
4053
4330
  }
4054
4331
  }
4055
- async tryEmbedFact(fact) {
4332
+ async tryEmbedFact(fact, ctx) {
4056
4333
  const embedFn = this.options.llmProvider.embed;
4057
4334
  if (typeof embedFn !== "function") return { ok: false, kind: "no_provider" };
4058
4335
  let tagsStr;
@@ -4074,6 +4351,7 @@ var EmbeddingService = class {
4074
4351
  const vector = await embedFn(text);
4075
4352
  if (vector.length === 0 || !vector.every((v) => typeof v === "number" && isFinite(v))) {
4076
4353
  console.warn(`[WikiMemory] embedFact: embed() returned an invalid vector for ${fact.id}; skipping.`);
4354
+ this.reportEmbed(ctx, fact, "embedding_failed", "invalid_vector");
4077
4355
  await this.markFailure(fact.id, "invalid_vector");
4078
4356
  return { ok: false, kind: "invalid_vector" };
4079
4357
  }
@@ -4087,11 +4365,13 @@ var EmbeddingService = class {
4087
4365
  }
4088
4366
  if (hasNonFinite) {
4089
4367
  console.warn(`[WikiMemory] embedFact: embed() returned values that overflow float32 for ${fact.id}; skipping.`);
4368
+ this.reportEmbed(ctx, fact, "embedding_failed", "float32_overflow");
4090
4369
  await this.markFailure(fact.id, "float32_overflow");
4091
4370
  return { ok: false, kind: "float32_overflow" };
4092
4371
  }
4093
4372
  } catch (err) {
4094
4373
  console.warn(`[WikiMemory] embedFact failed for ${fact.id}:`, err);
4374
+ this.reportEmbed(ctx, fact, "embedding_failed", "embed_threw");
4095
4375
  await this.markFailure(fact.id, "provider_error");
4096
4376
  return { ok: false, kind: "provider_error" };
4097
4377
  }
@@ -4101,15 +4381,31 @@ var EmbeddingService = class {
4101
4381
  await this.entryRepo.updateEmbeddingBlob(fact.id, blob);
4102
4382
  } catch (err) {
4103
4383
  console.warn(`[WikiMemory] embedFact: persisting embedding failed for ${fact.id}:`, err);
4384
+ this.reportEmbed(ctx, fact, "embedding_failed", "persist_failed");
4104
4385
  return { ok: false, kind: "storage_error" };
4105
4386
  }
4106
4387
  try {
4107
4388
  await this.notifyEmbeddingPersisted(fact.entity_id, fact.id, float32Vector);
4108
4389
  } catch (hookErr) {
4109
4390
  console.warn(`[WikiMemory] onEmbeddingPersisted hook failed for ${fact.id}:`, hookErr);
4391
+ this.reportEmbed(ctx, fact, "hook_failed", "on_embedding_persisted");
4110
4392
  }
4111
4393
  return { ok: true, dimension: float32Vector.length };
4112
4394
  }
4395
+ reportEmbed(ctx, fact, code, reason) {
4396
+ if (!ctx) return;
4397
+ emitDiagnostic(this.options, {
4398
+ code,
4399
+ operation: ctx.operation,
4400
+ trigger: ctx.trigger,
4401
+ entityId: fact.entity_id,
4402
+ detail: { factId: fact.id, reason }
4403
+ });
4404
+ }
4405
+ /** For callers that invoke `notifyEmbeddingPersisted` directly and catch its failure. */
4406
+ reportHookFailed(ctx, entityId, factId) {
4407
+ this.reportEmbed(ctx, { id: factId, entity_id: entityId }, "hook_failed", "on_embedding_persisted");
4408
+ }
4113
4409
  /** Marker writes must never fail the caller. Only marker-eligible kinds reach here. */
4114
4410
  async markFailure(id, kind) {
4115
4411
  try {
@@ -4123,8 +4419,8 @@ var EmbeddingService = class {
4123
4419
  * ImportExportService branch on `if (!embedded)`; a discriminated result
4124
4420
  * object is always truthy and would silently disable that branch.
4125
4421
  */
4126
- async embedFact(fact) {
4127
- const result = await this.tryEmbedFact(fact);
4422
+ async embedFact(fact, ctx) {
4423
+ const result = await this.tryEmbedFact(fact, ctx);
4128
4424
  return result.ok;
4129
4425
  }
4130
4426
  async notifyEmbeddingPersisted(entityId, factId, vector) {
@@ -4264,6 +4560,7 @@ function selectWithFloors(sortedRows, floors, maxResults) {
4264
4560
  }
4265
4561
 
4266
4562
  // src/services/RetrievalService.ts
4563
+ var EMPTY_ID_SET = /* @__PURE__ */ new Set();
4267
4564
  var RetrievalService = class {
4268
4565
  constructor(options, entryRepo, taskRepo, eventRepo, metadataRepo, searchService) {
4269
4566
  this.options = options;
@@ -4296,6 +4593,7 @@ var RetrievalService = class {
4296
4593
  }
4297
4594
  const rawMaxResults = options?.maxResults ?? config?.maxResults ?? config?.maxFtsResults ?? 10;
4298
4595
  const maxResults = Number.isFinite(rawMaxResults) ? Math.max(0, Math.trunc(rawMaxResults)) : 10;
4596
+ const excludeDrafts = (options?.excludeDrafts ?? config?.excludeDrafts ?? false) === true;
4299
4597
  const trimmedQuery = query.trim();
4300
4598
  const sanitizedTierFloors = trimmedQuery && exposeMetadata ? validateTierFloors(
4301
4599
  entityIds,
@@ -4315,6 +4613,9 @@ var RetrievalService = class {
4315
4613
  if (maxResults === 0) ; else if (trimmedQuery) {
4316
4614
  let usedEmbed = false;
4317
4615
  const scoredEntityIds = this._filterScoredEntities(entityIds, sanitizedTierWeights, options?.includeZeroWeightEntities);
4616
+ const draftIds = excludeDrafts && scoredEntityIds.length > 0 ? await this.entryRepo.findDraftIdsByEntityIds(scoredEntityIds) : EMPTY_ID_SET;
4617
+ const draftPad = draftIds.size;
4618
+ const padLimit = (n) => n >= Number.MAX_SAFE_INTEGER ? n : n + draftPad;
4318
4619
  if (scoredEntityIds.length === 0) {
4319
4620
  usedEmbed = true;
4320
4621
  } else if (!skipEmbed && embedFn) {
@@ -4348,7 +4649,7 @@ var RetrievalService = class {
4348
4649
  let miniSearchScores;
4349
4650
  if (effectivePreFilterLimit !== void 0) {
4350
4651
  populateCache = false;
4351
- const preResults = this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, Number.MAX_SAFE_INTEGER);
4652
+ const preResults = this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, Number.MAX_SAFE_INTEGER).filter((r) => !draftIds.has(r.id));
4352
4653
  if (preResults.length === 0) {
4353
4654
  candidateRows = null;
4354
4655
  } else {
@@ -4370,9 +4671,9 @@ var RetrievalService = class {
4370
4671
  }
4371
4672
  } else {
4372
4673
  if (useRanker) {
4373
- candidateRows = await this.entryRepo.findMetadataByEntityIds(scoredEntityIds);
4674
+ candidateRows = this._withoutDrafts(await this.entryRepo.findMetadataByEntityIds(scoredEntityIds), draftIds);
4374
4675
  } else {
4375
- candidateRows = await this.entryRepo.findWithEmbeddingsByEntityIds(scoredEntityIds);
4676
+ candidateRows = this._withoutDrafts(await this.entryRepo.findWithEmbeddingsByEntityIds(scoredEntityIds), draftIds);
4376
4677
  }
4377
4678
  if (weight !== void 0 && weight < 1) {
4378
4679
  miniSearchScores = this.searchService.getMiniSearchScores(trimmedQuery, scoredEntityIds);
@@ -4402,7 +4703,7 @@ var RetrievalService = class {
4402
4703
  candidateRows: rowsForEntity,
4403
4704
  weight,
4404
4705
  miniSearchScores,
4405
- limit: Math.max(maxResults * 2, maxResults + 50)
4706
+ limit: padLimit(Math.max(maxResults * 2, maxResults + 50))
4406
4707
  });
4407
4708
  return ranked.map((row) => ({ ...row, entity_id: scopedEntityId }));
4408
4709
  })
@@ -4616,7 +4917,7 @@ var RetrievalService = class {
4616
4917
  });
4617
4918
  } else if (policy === "keyword") {
4618
4919
  const hasActiveFloorsKeywordFallback = sanitizedTierFloors !== void 0 && Object.values(sanitizedTierFloors).some((f) => f > 0);
4619
- const keywordOversampledLimit = hasActiveFloorsKeywordFallback ? Number.MAX_SAFE_INTEGER : Math.max(maxResults * 2, maxResults + 50);
4920
+ const keywordOversampledLimit = hasActiveFloorsKeywordFallback ? Number.MAX_SAFE_INTEGER : padLimit(Math.max(maxResults * 2, maxResults + 50));
4620
4921
  const preFilteredIds = effectivePreFilterLimit !== void 0 ? new Set(candidateRows.map((r) => r.id)) : void 0;
4621
4922
  const keywordResults = this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, keywordOversampledLimit);
4622
4923
  const topResults = preFilteredIds === void 0 ? keywordResults : keywordResults.filter((r) => preFilteredIds.has(r.id));
@@ -4657,6 +4958,7 @@ var RetrievalService = class {
4657
4958
  // read() re-sorts after applying tier weights
4658
4959
  });
4659
4960
  }
4961
+ if (draftPad > 0) scored = scored.filter((s) => !draftIds.has(s.id));
4660
4962
  if (scored.length > 0) {
4661
4963
  scored = scored.map((row) => ({
4662
4964
  ...row,
@@ -4719,8 +5021,8 @@ var RetrievalService = class {
4719
5021
  }
4720
5022
  if (!usedEmbed && scoredEntityIds.length > 0) {
4721
5023
  const hasActiveFloors = sanitizedTierFloors !== void 0 && Object.values(sanitizedTierFloors).some((f) => f > 0);
4722
- const fallbackOversampledLimit = hasActiveFloors ? Number.MAX_SAFE_INTEGER : Math.max(maxResults * 2, maxResults + 50);
4723
- const results = this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, fallbackOversampledLimit);
5024
+ const fallbackOversampledLimit = hasActiveFloors ? Number.MAX_SAFE_INTEGER : padLimit(Math.max(maxResults * 2, maxResults + 50));
5025
+ const results = this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, fallbackOversampledLimit).filter((r) => !draftIds.has(r.id));
4724
5026
  const candidates = results.map((r) => ({
4725
5027
  id: r.id,
4726
5028
  entity_id: r.entity_id,
@@ -4744,7 +5046,7 @@ var RetrievalService = class {
4744
5046
  await this.entryRepo.trackAccess(ids, now);
4745
5047
  }
4746
5048
  } else {
4747
- facts = await this.entryRepo.findRecentByEntityIds(entityIds, maxResults);
5049
+ facts = excludeDrafts ? await this.entryRepo.findRecentByEntityIds(entityIds, maxResults, void 0, { excludeDrafts: true }) : await this.entryRepo.findRecentByEntityIds(entityIds, maxResults);
4748
5050
  }
4749
5051
  const eventsLimit = Math.min(10 * entityIds.length, 100);
4750
5052
  const [tasks, events] = await Promise.all([
@@ -4780,6 +5082,9 @@ var RetrievalService = class {
4780
5082
  _tieBreakSort(items) {
4781
5083
  items.sort((a, b) => this._compareScoredRows(a, b));
4782
5084
  }
5085
+ _withoutDrafts(rows, draftIds) {
5086
+ return draftIds.size === 0 ? rows : rows.filter((row) => !draftIds.has(row.id));
5087
+ }
4783
5088
  /**
4784
5089
  * Comparator for score + deterministic tie-break fields.
4785
5090
  * Negative return means "a ranks ahead of b" for descending score order.
@@ -4924,7 +5229,11 @@ var WriteService = class {
4924
5229
  if (shouldRunLibrarian) {
4925
5230
  try {
4926
5231
  this.jobManager.acquireLock("librarian", entityId);
4927
- this.runLibrarianThenMaybeHeal(entityId, librarianCount, prevMemoryCheckpoint).catch(console.error).finally(() => {
5232
+ const stage = { job: "librarian" };
5233
+ this.runLibrarianThenMaybeHeal(entityId, librarianCount, prevMemoryCheckpoint, stage).catch((err) => {
5234
+ console.error(err);
5235
+ this.reportBackgroundFailure(entityId, stage.job);
5236
+ }).finally(() => {
4928
5237
  this.jobManager.releaseLock("librarian", entityId);
4929
5238
  });
4930
5239
  } catch (e) {
@@ -4932,17 +5241,21 @@ var WriteService = class {
4932
5241
  await this.metadataRepo.updateCheckpoint(entityId, { memory: prevMemoryCheckpoint }, this.db);
4933
5242
  }
4934
5243
  } else if (!this.jobManager.isBlocked("librarian", entityId)) {
4935
- this.maybeRunHeal(entityId, eventCount).catch(console.error);
5244
+ this.maybeRunHeal(entityId, eventCount).catch((err) => {
5245
+ console.error(err);
5246
+ this.reportBackgroundFailure(entityId, "heal");
5247
+ });
4936
5248
  }
4937
5249
  }
4938
- async runLibrarianThenMaybeHeal(entityId, currentEventCount, prevCheckpoint) {
5250
+ async runLibrarianThenMaybeHeal(entityId, currentEventCount, prevCheckpoint, stage) {
4939
5251
  try {
4940
- await this.maintenanceService.doRunLibrarian(entityId);
5252
+ await this.maintenanceService.doRunLibrarian(entityId, void 0, "auto");
4941
5253
  await this.metadataRepo.updateCheckpoint(entityId, { memory: currentEventCount }, this.db);
4942
5254
  } catch (e) {
4943
5255
  await this.metadataRepo.updateCheckpoint(entityId, { memory: prevCheckpoint }, this.db);
4944
5256
  throw e;
4945
5257
  }
5258
+ stage.job = "heal";
4946
5259
  await this.maybeRunHeal(entityId, currentEventCount);
4947
5260
  }
4948
5261
  /**
@@ -4958,7 +5271,7 @@ var WriteService = class {
4958
5271
  const shouldRunHeal = currentEventCount - healCheckpoint >= autoHealThreshold;
4959
5272
  if (shouldRunHeal && this.jobManager.tryAcquireAutoHealLock(entityId)) {
4960
5273
  try {
4961
- const result = await this.maintenanceService.doRunHeal(entityId);
5274
+ const result = await this.maintenanceService.doRunHeal(entityId, { trigger: "auto" });
4962
5275
  if (result.remaining === 0) {
4963
5276
  await this.metadataRepo.updateCheckpoint(entityId, { heal: currentEventCount }, this.db);
4964
5277
  }
@@ -4967,8 +5280,17 @@ var WriteService = class {
4967
5280
  }
4968
5281
  }
4969
5282
  }
5283
+ reportBackgroundFailure(entityId, job) {
5284
+ emitDiagnostic(this.options, {
5285
+ code: "background_job_failed",
5286
+ operation: job,
5287
+ trigger: "auto",
5288
+ entityId,
5289
+ detail: { reason: "unhandled_rejection" }
5290
+ });
5291
+ }
4970
5292
  };
4971
5293
 
4972
- export { BaseRepository, DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, DEFAULT_MAX_EMBED_CHARS, 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, WikiDuplicateHashError, WikiGraphNodeOwnershipConflict, WikiIngestEmptyError, WikiInvalidReadOptions, WikiParseError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiTransactionError, WriteService, __privateAdd, __privateGet, __privateSet, chunkText, configureRandomSource, emptyManifest, entitySummaryMetaKey, extractSqliteCode, generateId, normalizeSourceHash, normalizeSourceRef, normalizeTitleKey, parseEmbedding, resolveEdgeDefinitions, resolveNodeType, safeSlice, typeSatisfies, validateInlineEdges, validateManifest };
4973
- //# sourceMappingURL=chunk-TTECJ3S3.mjs.map
4974
- //# sourceMappingURL=chunk-TTECJ3S3.mjs.map
5294
+ 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, emptyManifest, entitySummaryMetaKey, extractSqliteCode, generateId, normalizeSourceHash, normalizeSourceRef, normalizeTitleKey, parseEmbedding, resolveEdgeDefinitions, resolveNodeType, safeSlice, typeSatisfies, validateInlineEdges, validateManifest };
5295
+ //# sourceMappingURL=chunk-G5OR2VWD.mjs.map
5296
+ //# sourceMappingURL=chunk-G5OR2VWD.mjs.map