@equationalapplications/core-llm-wiki 7.2.0 → 7.3.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.
@@ -336,20 +336,27 @@ function mergeOntologyUpdates(current, updates) {
336
336
  function validateInlineEdges(sourceType, _targetType, edges, manifest, opts) {
337
337
  const strict = opts?.strict === true;
338
338
  const entityId = opts?.entityId ?? "";
339
+ const drops = opts?.drops;
340
+ const drop = (reason, edgeType) => {
341
+ drops?.push({ reason, sourceId: null, edgeType, sourceNodeType: sourceType, targetNodeType: null });
342
+ };
339
343
  if (!Array.isArray(edges)) {
340
344
  if (strict) throw new WikiStrictOntologyViolation(entityId, "edge", "");
345
+ drop("invalid_shape", null);
341
346
  return [];
342
347
  }
343
348
  const valid = [];
344
349
  for (const edge of edges) {
345
350
  if (typeof edge?.edge_type !== "string" || typeof edge?.target_title !== "string") {
346
351
  if (strict) throw new WikiStrictOntologyViolation(entityId, "edge", String(edge?.edge_type ?? ""));
352
+ drop("invalid_shape", typeof edge?.edge_type === "string" ? edge.edge_type : null);
347
353
  continue;
348
354
  }
349
355
  const defs = resolveEdgeDefinitions(edge.edge_type, manifest);
350
356
  const match = defs.find((d) => typeSatisfies(d.source_type, sourceType, manifest));
351
357
  if (!match) {
352
358
  if (strict) throw new WikiStrictOntologyViolation(entityId, "edge", edge.edge_type);
359
+ drop("type_not_in_manifest", edge.edge_type);
353
360
  continue;
354
361
  }
355
362
  valid.push({ edge_type: match.type, target_title: edge.target_title });
@@ -1526,6 +1533,17 @@ function validateTask(task) {
1526
1533
  priority
1527
1534
  };
1528
1535
  }
1536
+ function factRejectionReason(raw) {
1537
+ const fact = raw;
1538
+ if (typeof fact?.title !== "string" || typeof fact?.body !== "string") return "invalid_shape";
1539
+ if (!clip(fact.title, 80)) return "missing_title";
1540
+ return "missing_body";
1541
+ }
1542
+ function taskRejectionReason(raw) {
1543
+ const task = raw;
1544
+ if (typeof task?.description !== "string") return "invalid_shape";
1545
+ return "missing_description";
1546
+ }
1529
1547
  function normalizeSourceRef(value) {
1530
1548
  if (typeof value !== "string") return null;
1531
1549
  const cleaned = value.replace(/[^A-Za-z0-9._\- ]/g, "").trim().slice(0, 255);
@@ -1765,6 +1783,104 @@ function applyBodyTruncation(candidates, attemptLevel, bodyTruncationChars) {
1765
1783
  var DEFAULT_MAX_CHUNK_LENGTH = 12e3;
1766
1784
  var DEFAULT_CHUNK_OVERLAP = 400;
1767
1785
 
1786
+ // src/utils/diagnostics.ts
1787
+ var SEVERITY = {
1788
+ ingest_chunk_failed: "warn",
1789
+ fact_rejected: "warn",
1790
+ task_rejected: "warn",
1791
+ fact_deduplicated: "info",
1792
+ edge_dropped: "warn",
1793
+ embedding_failed: "warn",
1794
+ hook_failed: "warn",
1795
+ background_job_failed: "error",
1796
+ heal_skipped: "warn",
1797
+ grounding_missing: "warn",
1798
+ grounding_failed: "warn",
1799
+ classification_low_confidence: "info",
1800
+ classification_invalid: "warn"
1801
+ };
1802
+ var MESSAGE = {
1803
+ ingest_chunk_failed: "One or more document chunks failed to parse or generate.",
1804
+ fact_rejected: "An extracted fact failed validation and was not written.",
1805
+ task_rejected: "An extracted task failed validation and was not written.",
1806
+ fact_deduplicated: "An extracted fact duplicated an existing fact and was not written.",
1807
+ edge_dropped: "An extracted edge could not be resolved against the ontology and was not written.",
1808
+ embedding_failed: "A fact could not be embedded.",
1809
+ hook_failed: "A host hook threw or rejected.",
1810
+ background_job_failed: "A background maintenance job failed.",
1811
+ heal_skipped: "Heal skipped a candidate fact.",
1812
+ grounding_missing: "A fact carried no qualifying evidence and was stored as a draft.",
1813
+ grounding_failed: "A fact carried evidence not found in its source and was stored as a draft.",
1814
+ classification_low_confidence: "A classifier answer was below the confidence threshold and was not applied.",
1815
+ classification_invalid: "A classifier answer was invalid and was not applied."
1816
+ };
1817
+ var warnedNonFunction = /* @__PURE__ */ new WeakSet();
1818
+ function copyDetail(detail) {
1819
+ const copy = { ...detail };
1820
+ if (detail.chunkIndexes) copy.chunkIndexes = detail.chunkIndexes.slice();
1821
+ return copy;
1822
+ }
1823
+ function emitDiagnostic(options, input) {
1824
+ const hook = options.onDiagnostic;
1825
+ if (hook === void 0 || hook === null) return;
1826
+ if (typeof hook !== "function") {
1827
+ if (!warnedNonFunction.has(options)) {
1828
+ warnedNonFunction.add(options);
1829
+ console.warn("[WikiMemory] onDiagnostic is not a function; diagnostics are disabled.");
1830
+ }
1831
+ return;
1832
+ }
1833
+ const diagnostic = {
1834
+ code: input.code,
1835
+ severity: SEVERITY[input.code],
1836
+ operation: input.operation,
1837
+ trigger: input.trigger,
1838
+ entityId: input.entityId,
1839
+ at: Date.now(),
1840
+ message: MESSAGE[input.code],
1841
+ ...input.detail ? { detail: copyDetail(input.detail) } : {}
1842
+ };
1843
+ try {
1844
+ const returned = hook(diagnostic);
1845
+ if (returned !== void 0) {
1846
+ Promise.resolve(returned).catch((err) => {
1847
+ console.warn("[WikiMemory] onDiagnostic hook rejected:", err);
1848
+ });
1849
+ }
1850
+ } catch (err) {
1851
+ console.warn("[WikiMemory] onDiagnostic hook threw:", err);
1852
+ }
1853
+ }
1854
+ var DiagnosticBuffer = class {
1855
+ constructor() {
1856
+ this.items = [];
1857
+ }
1858
+ push(input) {
1859
+ this.items.push(input);
1860
+ }
1861
+ get size() {
1862
+ return this.items.length;
1863
+ }
1864
+ flush(options) {
1865
+ const pending = this.items;
1866
+ this.items = [];
1867
+ for (const input of pending) emitDiagnostic(options, input);
1868
+ }
1869
+ discard() {
1870
+ this.items = [];
1871
+ }
1872
+ };
1873
+ function edgeDropDiagnostic(drop, ctx) {
1874
+ const detail = { reason: drop.reason };
1875
+ const factId = ctx.factId ?? drop.sourceId;
1876
+ if (factId) detail.factId = factId;
1877
+ if (drop.edgeType) detail.edgeType = drop.edgeType;
1878
+ if (drop.sourceNodeType) detail.sourceNodeType = drop.sourceNodeType;
1879
+ if (drop.targetNodeType) detail.targetNodeType = drop.targetNodeType;
1880
+ if (ctx.sourceRef) detail.sourceRef = ctx.sourceRef;
1881
+ return { code: "edge_dropped", operation: ctx.operation, trigger: ctx.trigger, entityId: ctx.entityId, detail };
1882
+ }
1883
+
1768
1884
  // src/services/IngestionService.ts
1769
1885
  function zeroChunkResult(duplicateOf) {
1770
1886
  const result = {
@@ -1832,9 +1948,24 @@ var IngestionService = class {
1832
1948
  try {
1833
1949
  const responseText = await this.options.llmProvider.generateText({ systemPrompt, userPrompt });
1834
1950
  const result2 = parseJsonResponse(responseText);
1951
+ const rawFacts = Array.isArray(result2.facts) ? result2.facts : [];
1952
+ const facts = [];
1953
+ const itemIndexes = [];
1954
+ const rejected = [];
1955
+ rawFacts.forEach((raw, itemIndex) => {
1956
+ const valid = validateFact(raw);
1957
+ if (valid) {
1958
+ facts.push(valid);
1959
+ itemIndexes.push(itemIndex);
1960
+ } else {
1961
+ rejected.push({ itemIndex, reason: factRejectionReason(raw) });
1962
+ }
1963
+ });
1835
1964
  return {
1836
1965
  status: "ok",
1837
- facts: (Array.isArray(result2.facts) ? result2.facts : []).map(validateFact).filter((f) => f !== null),
1966
+ facts,
1967
+ itemIndexes,
1968
+ rejected,
1838
1969
  ontology_updates: result2.ontology_updates
1839
1970
  };
1840
1971
  } catch (e) {
@@ -1879,23 +2010,48 @@ var IngestionService = class {
1879
2010
  const failures = [];
1880
2011
  const seen = /* @__PURE__ */ new Set();
1881
2012
  const orderedChunkFacts = [];
1882
- for (const slot of chunkResults) {
2013
+ const diagBuffer = new DiagnosticBuffer();
2014
+ const diagBase = { entityId, operation: "ingest", trigger: "call" };
2015
+ for (const [chunkIndex, slot] of chunkResults.entries()) {
1883
2016
  if (slot.status === "failed") {
1884
2017
  failedChunks++;
1885
2018
  failures.push(slot.error);
1886
2019
  continue;
1887
2020
  }
1888
2021
  ingestedChunks++;
2022
+ for (const r of slot.rejected) {
2023
+ diagBuffer.push({ ...diagBase, code: "fact_rejected", detail: { sourceRef, chunkIndex, itemIndex: r.itemIndex, reason: r.reason } });
2024
+ }
1889
2025
  const dedupedFacts = [];
1890
- for (const fact of slot.facts) {
2026
+ slot.facts.forEach((fact, k) => {
1891
2027
  const normalizedTitle = normalizeTitleKey(fact.title);
1892
2028
  if (!seen.has(normalizedTitle)) {
1893
2029
  seen.add(normalizedTitle);
1894
2030
  dedupedFacts.push(fact);
2031
+ } else {
2032
+ diagBuffer.push({
2033
+ ...diagBase,
2034
+ code: "fact_deduplicated",
2035
+ detail: { sourceRef, chunkIndex, itemIndex: slot.itemIndexes[k], reason: "exact_title" }
2036
+ });
1895
2037
  }
1896
- }
2038
+ });
1897
2039
  orderedChunkFacts.push({ facts: dedupedFacts, ontology_updates: slot.ontology_updates });
1898
2040
  }
2041
+ for (const reason of ["parse", "llm"]) {
2042
+ const ofReason = failures.filter((f) => f.source === reason);
2043
+ if (ofReason.length === 0) continue;
2044
+ diagBuffer.push({
2045
+ ...diagBase,
2046
+ code: "ingest_chunk_failed",
2047
+ detail: {
2048
+ sourceRef,
2049
+ reason,
2050
+ count: ofReason.length,
2051
+ chunkIndexes: ofReason.map((f) => f.chunkIndex).sort((a, b) => a - b).slice(0, 20)
2052
+ }
2053
+ });
2054
+ }
1899
2055
  if (failedChunks === chunks.length) {
1900
2056
  throw new WikiIngestEmptyError({
1901
2057
  parseFailures: failures,
@@ -1908,7 +2064,7 @@ var IngestionService = class {
1908
2064
  try {
1909
2065
  if (failedChunks === 0) {
1910
2066
  const fullResult = await this.db.withTransactionAsync(async (tx) => {
1911
- return await this.runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx);
2067
+ return await this.runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx, diagBuffer);
1912
2068
  });
1913
2069
  deletedSourceFactIds.push(...fullResult.deletedSourceFactIds);
1914
2070
  insertedFacts.push(...fullResult.insertedFacts);
@@ -1916,7 +2072,7 @@ var IngestionService = class {
1916
2072
  const partialResult = await this.db.withTransactionAsync(async (tx) => {
1917
2073
  const flat = [];
1918
2074
  for (const slot of orderedChunkFacts) flat.push(...slot.facts);
1919
- return await this.appendPartialFacts(entityId, sourceRef, flat, tx);
2075
+ return await this.appendPartialFacts(entityId, sourceRef, flat, tx, diagBuffer);
1920
2076
  });
1921
2077
  insertedFacts.push(...partialResult.insertedDescriptors);
1922
2078
  }
@@ -1930,6 +2086,7 @@ var IngestionService = class {
1930
2086
  }
1931
2087
  return zeroChunkResult(canonical);
1932
2088
  }
2089
+ diagBuffer.flush(this.options);
1933
2090
  await this.searchService.sync(entityId);
1934
2091
  if (failedChunks === 0) {
1935
2092
  const uniqueDeletedSourceFactIds = Array.from(new Set(deletedSourceFactIds));
@@ -1938,11 +2095,18 @@ var IngestionService = class {
1938
2095
  await this.embeddingService.notifyEmbeddingPersisted(entityId, factId, null);
1939
2096
  } catch (hookErr) {
1940
2097
  console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during ingest for ${factId}:`, hookErr);
2098
+ emitDiagnostic(this.options, {
2099
+ entityId,
2100
+ operation: "ingest",
2101
+ trigger: "call",
2102
+ code: "hook_failed",
2103
+ detail: { factId, reason: "on_embedding_persisted" }
2104
+ });
1941
2105
  }
1942
2106
  }
1943
2107
  }
1944
2108
  for (const fact of insertedFacts) {
1945
- await this.embeddingService.embedFact(fact);
2109
+ await this.embeddingService.embedFact(fact, { operation: "ingest", trigger: "call" });
1946
2110
  }
1947
2111
  this.searchService.evictCache(entityId);
1948
2112
  const result = {
@@ -2088,6 +2252,19 @@ var IngestionService = class {
2088
2252
  const match = candidates[0];
2089
2253
  if (!match) {
2090
2254
  if (strictEffective) throw new WikiStrictOntologyViolation(entityId, "edge", edge.type);
2255
+ opts?.diag?.buffer.push({
2256
+ entityId,
2257
+ operation: opts.diag.operation,
2258
+ trigger: "call",
2259
+ code: "edge_dropped",
2260
+ detail: {
2261
+ reason: "manifest_violation",
2262
+ factId: edge.sourceId,
2263
+ edgeType: edge.type,
2264
+ ...sourceType ? { sourceNodeType: sourceType } : {},
2265
+ sourceRef: params.sourceRef
2266
+ }
2267
+ });
2091
2268
  continue;
2092
2269
  }
2093
2270
  validEdges.push({
@@ -2128,7 +2305,7 @@ var IngestionService = class {
2128
2305
  * On the partial path the caller never invokes this method; the empty
2129
2306
  * arrays stay empty.
2130
2307
  */
2131
- async runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx) {
2308
+ async runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx, diagBuffer) {
2132
2309
  const deletedSourceFactIds = [];
2133
2310
  const insertedFacts = [];
2134
2311
  deletedSourceFactIds.push(...await this.entryRepo.findIdsBySource(entityId, sourceRef, null, tx, false));
@@ -2153,8 +2330,12 @@ var IngestionService = class {
2153
2330
  }
2154
2331
  for (const fact of facts) {
2155
2332
  const ontologyFact = fact;
2156
- const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false }) ?? { okf_type: null, edges: [] };
2157
2333
  const id = generateId("fact_");
2334
+ const validationDrops = [];
2335
+ const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false, drops: validationDrops }) ?? { okf_type: null, edges: [] };
2336
+ for (const drop of validationDrops) {
2337
+ diagBuffer.push(edgeDropDiagnostic(drop, { entityId, operation: "ingest", trigger: "call", sourceRef, factId: id }));
2338
+ }
2158
2339
  hostNodes.push({
2159
2340
  id,
2160
2341
  type: ontologyFact.okf_type ?? "",
@@ -2179,6 +2360,7 @@ var IngestionService = class {
2179
2360
  }
2180
2361
  const hostEdges = [];
2181
2362
  for (const req of rawEdgeRequests) {
2363
+ const resolveDrops = [];
2182
2364
  const resolved = this.ontologyService?.resolveEdges(
2183
2365
  entityId,
2184
2366
  req.sourceId,
@@ -2186,8 +2368,12 @@ var IngestionService = class {
2186
2368
  req.edges,
2187
2369
  manifest,
2188
2370
  titleIndex,
2189
- now
2371
+ now,
2372
+ resolveDrops
2190
2373
  ) ?? [];
2374
+ for (const drop of resolveDrops) {
2375
+ diagBuffer.push(edgeDropDiagnostic(drop, { entityId, operation: "ingest", trigger: "call", sourceRef }));
2376
+ }
2191
2377
  for (const e of resolved) {
2192
2378
  hostEdges.push({ type: e.edge_type, sourceId: e.source_id, targetId: e.target_id });
2193
2379
  }
@@ -2196,7 +2382,7 @@ var IngestionService = class {
2196
2382
  entityId,
2197
2383
  { sourceRef, sourceHash, nodes: hostNodes, edges: hostEdges },
2198
2384
  tx,
2199
- { strict: false }
2385
+ { strict: false, diag: { buffer: diagBuffer, operation: "ingest" } }
2200
2386
  );
2201
2387
  return { deletedSourceFactIds, insertedFacts };
2202
2388
  }
@@ -2220,7 +2406,7 @@ var IngestionService = class {
2220
2406
  * Runs INSIDE the caller's `tx`. Does not open a nested transaction.
2221
2407
  * Returns `{ inserted, skippedDuplicate }` for observability.
2222
2408
  */
2223
- async appendPartialFacts(entityId, sourceRef, dedupedFacts, tx) {
2409
+ async appendPartialFacts(entityId, sourceRef, dedupedFacts, tx, diagBuffer) {
2224
2410
  const liveIds = await this.entryRepo.findIdsBySource(entityId, sourceRef, null, tx, false);
2225
2411
  const liveFacts = liveIds.length === 0 ? [] : await this.entryRepo.findByIds(liveIds, void 0, tx);
2226
2412
  const liveTitles = new Set(liveFacts.map((f) => normalizeTitleKey(f.title)));
@@ -2231,6 +2417,13 @@ var IngestionService = class {
2231
2417
  for (const fact of dedupedFacts) {
2232
2418
  const normalizedTitle = normalizeTitleKey(fact.title);
2233
2419
  if (liveTitles.has(normalizedTitle)) {
2420
+ diagBuffer.push({
2421
+ entityId,
2422
+ operation: "ingest",
2423
+ trigger: "call",
2424
+ code: "fact_deduplicated",
2425
+ detail: { sourceRef, reason: "exact_title" }
2426
+ });
2234
2427
  skippedDuplicate++;
2235
2428
  continue;
2236
2429
  }
@@ -2817,7 +3010,7 @@ var MaintenanceService = class {
2817
3010
  permanentlyFailed++;
2818
3011
  continue;
2819
3012
  }
2820
- const result = await this.embeddingService.tryEmbedFact(row);
3013
+ const result = await this.embeddingService.tryEmbedFact(row, { operation: "reembed", trigger: "call" });
2821
3014
  if (result.ok) embedded++;
2822
3015
  else failed++;
2823
3016
  }
@@ -2938,7 +3131,7 @@ var MaintenanceService = class {
2938
3131
  return { deleted: { entries, tasks: 0 } };
2939
3132
  }
2940
3133
  /** Core librarian pass (locks handled by {@link runLibrarian}). Package-internal orchestration hook. */
2941
- async doRunLibrarian(entityId, promptOverride) {
3134
+ async doRunLibrarian(entityId, promptOverride, trigger = "call") {
2942
3135
  const events = await this.eventRepo.getRecent(entityId, 50);
2943
3136
  const currentFactsRows = await this.entryRepo.findRecentByEntityId(entityId, 100);
2944
3137
  const currentFacts = currentFactsRows.map((f) => {
@@ -2960,8 +3153,25 @@ var MaintenanceService = class {
2960
3153
  const facts = Array.isArray(result.facts) ? result.facts : [];
2961
3154
  const tasks = Array.isArray(result.tasks) ? result.tasks : [];
2962
3155
  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);
3156
+ const diagBuffer = new DiagnosticBuffer();
3157
+ const diagBase = { entityId, operation: "librarian", trigger };
3158
+ const validFacts = [];
3159
+ const validFactItemIndexes = [];
3160
+ facts.forEach((raw, itemIndex) => {
3161
+ const valid = validateFact(raw);
3162
+ if (valid) {
3163
+ validFacts.push(valid);
3164
+ validFactItemIndexes.push(itemIndex);
3165
+ } else {
3166
+ diagBuffer.push({ ...diagBase, code: "fact_rejected", detail: { itemIndex, reason: factRejectionReason(raw) } });
3167
+ }
3168
+ });
3169
+ const validTasks = [];
3170
+ tasks.forEach((raw, itemIndex) => {
3171
+ const valid = validateTask(raw);
3172
+ if (valid) validTasks.push(valid);
3173
+ else diagBuffer.push({ ...diagBase, code: "task_rejected", detail: { itemIndex, reason: taskRejectionReason(raw) } });
3174
+ });
2965
3175
  const now = Date.now();
2966
3176
  const insertedFacts = [];
2967
3177
  await this.db.withTransactionAsync(async (tx) => {
@@ -2978,7 +3188,7 @@ var MaintenanceService = class {
2978
3188
  }
2979
3189
  const factsForDedupe = await this.entryRepo.findRecentByEntityId(entityId, 100, tx);
2980
3190
  const pendingEdges = [];
2981
- for (const fact of validFacts) {
3191
+ for (const [k, fact] of validFacts.entries()) {
2982
3192
  const newTokens = titleTokens(fact.title);
2983
3193
  let skip = false;
2984
3194
  if (newTokens.size >= MIN_TOKENS_TO_QUALIFY) {
@@ -2993,10 +3203,15 @@ var MaintenanceService = class {
2993
3203
  }
2994
3204
  }
2995
3205
  }
2996
- if (skip) continue;
3206
+ if (skip) {
3207
+ diagBuffer.push({ ...diagBase, code: "fact_deduplicated", detail: { itemIndex: validFactItemIndexes[k], reason: "fuzzy_title" } });
3208
+ continue;
3209
+ }
2997
3210
  const ontologyFact = fact;
2998
- const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false }) ?? { okf_type: null, edges: [] };
2999
3211
  const id = generateId("fact_");
3212
+ const validationDrops = [];
3213
+ const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false, drops: validationDrops }) ?? { okf_type: null, edges: [] };
3214
+ for (const drop of validationDrops) diagBuffer.push(edgeDropDiagnostic(drop, { ...diagBase, factId: id }));
3000
3215
  const factObj = {
3001
3216
  id,
3002
3217
  entity_id: entityId,
@@ -3023,6 +3238,7 @@ var MaintenanceService = class {
3023
3238
  }
3024
3239
  }
3025
3240
  for (const item of pendingEdges) {
3241
+ const resolveDrops = [];
3026
3242
  await this.ontologyService?.resolveAndPersistEdges(
3027
3243
  entityId,
3028
3244
  item.sourceId,
@@ -3031,8 +3247,10 @@ var MaintenanceService = class {
3031
3247
  manifest,
3032
3248
  titleIndex,
3033
3249
  tx,
3034
- now
3250
+ now,
3251
+ resolveDrops
3035
3252
  );
3253
+ for (const drop of resolveDrops) diagBuffer.push(edgeDropDiagnostic(drop, diagBase));
3036
3254
  }
3037
3255
  for (const task of validTasks) {
3038
3256
  const id = generateId("task_");
@@ -3050,9 +3268,10 @@ var MaintenanceService = class {
3050
3268
  await this.taskRepo.upsert(taskObj, tx);
3051
3269
  }
3052
3270
  });
3271
+ diagBuffer.flush(this.options);
3053
3272
  await this.searchService.sync(entityId);
3054
3273
  for (const fact of insertedFacts) {
3055
- await this.embeddingService.embedFact(fact);
3274
+ await this.embeddingService.embedFact(fact, { operation: "librarian", trigger });
3056
3275
  }
3057
3276
  this.searchService.evictCache(entityId);
3058
3277
  }
@@ -3066,6 +3285,8 @@ var MaintenanceService = class {
3066
3285
  async doRunHeal(entityId, options) {
3067
3286
  const promptOverride = options?.promptOverride;
3068
3287
  const batchSize = options?.batchSize ?? HEAL_BATCH_SIZE;
3288
+ const trigger = options?.trigger ?? "call";
3289
+ const diagBase = { entityId, operation: "heal", trigger };
3069
3290
  if (!Number.isInteger(batchSize) || batchSize < 1) {
3070
3291
  throw new Error("Invalid batchSize: must be an integer >= 1");
3071
3292
  }
@@ -3101,6 +3322,7 @@ var MaintenanceService = class {
3101
3322
  await this.embeddingService.notifyEmbeddingPersisted(entityId, factId, null);
3102
3323
  } catch (hookErr) {
3103
3324
  console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during heal orphan pass for ${factId}:`, hookErr);
3325
+ emitDiagnostic(this.options, { ...diagBase, code: "hook_failed", detail: { factId, reason: "on_embedding_persisted" } });
3104
3326
  }
3105
3327
  }
3106
3328
  const healCandidates = await this.entryRepo.findHealCandidatesByEntityId(entityId, batchSize, recheckCutoff);
@@ -3173,22 +3395,38 @@ var MaintenanceService = class {
3173
3395
  const safeDowngradedSet = /* @__PURE__ */ new Set();
3174
3396
  const safeDeletedSet = /* @__PURE__ */ new Set();
3175
3397
  const newFacts = [];
3398
+ const newFactItemIndexes = [];
3176
3399
  for (const batchResult of outcome.results) {
3177
3400
  const mutableIds = new Set(batchResult.batch.map((f) => f.id));
3178
3401
  for (const id of batchResult.downgraded) if (mutableIds.has(id)) safeDowngradedSet.add(id);
3179
3402
  for (const id of batchResult.deleted) if (mutableIds.has(id)) safeDeletedSet.add(id);
3180
- newFacts.push(...batchResult.newFacts);
3403
+ batchResult.newFacts.forEach((raw, itemIndex) => {
3404
+ newFacts.push(raw);
3405
+ newFactItemIndexes.push(itemIndex);
3406
+ });
3181
3407
  }
3182
3408
  const safeDowngraded = Array.from(safeDowngradedSet);
3183
3409
  const safeDeleted = Array.from(safeDeletedSet);
3184
- const validNewFacts = newFacts.map(validateFact).filter((f) => f !== null);
3410
+ const diagBuffer = new DiagnosticBuffer();
3411
+ const validNewFacts = [];
3412
+ const validNewFactItemIndexes = [];
3413
+ newFacts.forEach((raw, k) => {
3414
+ const itemIndex = newFactItemIndexes[k];
3415
+ const valid = validateFact(raw);
3416
+ if (valid) {
3417
+ validNewFacts.push(valid);
3418
+ validNewFactItemIndexes.push(itemIndex);
3419
+ } else {
3420
+ diagBuffer.push({ ...diagBase, code: "fact_rejected", detail: { itemIndex, reason: factRejectionReason(raw) } });
3421
+ }
3422
+ });
3185
3423
  const insertedFacts = [];
3186
3424
  const uniqueDeletedFactIds = Array.from(new Set(safeDeleted));
3187
3425
  await this.db.withTransactionAsync(async (tx) => {
3188
3426
  await this.entryRepo.downgradeByIds(safeDowngraded, entityId, tx);
3189
3427
  await this.entryRepo.softDeleteByIds(safeDeleted, entityId, tx);
3190
3428
  const healFactsForDedupe = await this.entryRepo.findInferredTitlesByEntityId(entityId, tx);
3191
- for (const fact of validNewFacts) {
3429
+ for (const [k, fact] of validNewFacts.entries()) {
3192
3430
  const newTokens = titleTokens(fact.title);
3193
3431
  let skip = false;
3194
3432
  if (newTokens.size >= MIN_TOKENS_TO_QUALIFY) {
@@ -3202,7 +3440,10 @@ var MaintenanceService = class {
3202
3440
  }
3203
3441
  }
3204
3442
  }
3205
- if (skip) continue;
3443
+ if (skip) {
3444
+ diagBuffer.push({ ...diagBase, code: "fact_deduplicated", detail: { itemIndex: validNewFactItemIndexes[k], reason: "fuzzy_title" } });
3445
+ continue;
3446
+ }
3206
3447
  const id = generateId("fact_");
3207
3448
  const factObj = {
3208
3449
  id,
@@ -3237,16 +3478,21 @@ var MaintenanceService = class {
3237
3478
  tx
3238
3479
  );
3239
3480
  });
3481
+ for (const { item, reason } of outcome.skipped) {
3482
+ diagBuffer.push({ ...diagBase, code: "heal_skipped", detail: { factId: item.id, reason } });
3483
+ }
3484
+ diagBuffer.flush(this.options);
3240
3485
  await this.searchService.sync(entityId);
3241
3486
  for (const factId of uniqueDeletedFactIds) {
3242
3487
  try {
3243
3488
  await this.embeddingService.notifyEmbeddingPersisted(entityId, factId, null);
3244
3489
  } catch (hookErr) {
3245
3490
  console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during heal for ${factId}:`, hookErr);
3491
+ emitDiagnostic(this.options, { ...diagBase, code: "hook_failed", detail: { factId, reason: "on_embedding_persisted" } });
3246
3492
  }
3247
3493
  }
3248
3494
  for (const fact of insertedFacts) {
3249
- await this.embeddingService.embedFact(fact);
3495
+ await this.embeddingService.embedFact(fact, { operation: "heal", trigger });
3250
3496
  }
3251
3497
  this.searchService.evictCache(entityId);
3252
3498
  const skippedIds = new Set(outcome.skipped.map(({ item }) => item.id));
@@ -3386,6 +3632,8 @@ var MaintenanceService = class {
3386
3632
  let failedValidation = 0;
3387
3633
  let edgesAdded = 0;
3388
3634
  let abortedOntologyOff = false;
3635
+ const diagBuffer = new DiagnosticBuffer();
3636
+ const diagBase = { entityId, operation: "ontologyBackfill", trigger: "call" };
3389
3637
  await this.db.withTransactionAsync(async (tx) => {
3390
3638
  let { mode: txMode, manifest } = await ontologyService.getEffectiveState(entityId, tx);
3391
3639
  if (txMode === "off") {
@@ -3410,6 +3658,7 @@ var MaintenanceService = class {
3410
3658
  continue;
3411
3659
  }
3412
3660
  applied.add(fact.id);
3661
+ const validationDrops = [];
3413
3662
  const normalized = ontologyService.validateAndNormalizeFact(
3414
3663
  {
3415
3664
  title: fact.title,
@@ -3420,8 +3669,11 @@ var MaintenanceService = class {
3420
3669
  edges: classification.edges
3421
3670
  },
3422
3671
  manifest,
3423
- { strict: false }
3672
+ { strict: false, drops: validationDrops }
3424
3673
  );
3674
+ for (const drop of validationDrops) {
3675
+ diagBuffer.push(edgeDropDiagnostic(drop, { ...diagBase, factId: fact.id }));
3676
+ }
3425
3677
  if (!normalized.okf_type) {
3426
3678
  failedValidation++;
3427
3679
  continue;
@@ -3435,7 +3687,8 @@ var MaintenanceService = class {
3435
3687
  }
3436
3688
  }
3437
3689
  for (const item of pendingEdges) {
3438
- edgesAdded += await ontologyService.resolveAndPersistEdges(
3690
+ const resolveDrops = [];
3691
+ const added = await ontologyService.resolveAndPersistEdges(
3439
3692
  entityId,
3440
3693
  item.sourceId,
3441
3694
  item.sourceType,
@@ -3443,11 +3696,17 @@ var MaintenanceService = class {
3443
3696
  manifest,
3444
3697
  titleIndex,
3445
3698
  tx,
3446
- now
3699
+ now,
3700
+ resolveDrops
3447
3701
  );
3702
+ edgesAdded += added;
3703
+ for (const drop of resolveDrops) {
3704
+ diagBuffer.push(edgeDropDiagnostic(drop, diagBase));
3705
+ }
3448
3706
  }
3449
3707
  await this.entryRepo.markOntologyChecked(batch.map((f) => f.id), entityId, now, tx);
3450
3708
  });
3709
+ diagBuffer.flush(this.options);
3451
3710
  return { typed, failedValidation, edgesAdded, abortedOntologyOff };
3452
3711
  }
3453
3712
  _validatePruneDuration(value, name) {
@@ -3849,7 +4108,7 @@ var ImportExportService = class {
3849
4108
  title: clipped?.title ?? fact.title,
3850
4109
  body: clipped?.body ?? fact.body,
3851
4110
  tags: Array.isArray(fact.tags) || typeof fact.tags === "string" ? fact.tags : []
3852
- });
4111
+ }, { operation: "importDump", trigger: "call" });
3853
4112
  if (!embedded) {
3854
4113
  await this.embeddingService.notifyEmbeddingPersisted(entityId, fact.id, null);
3855
4114
  }
@@ -3874,6 +4133,11 @@ var ImportExportService = class {
3874
4133
  `[WikiMemory] onEmbeddingPersisted hook failed for preserved-blob fact ${fact.id}:`,
3875
4134
  hookErr
3876
4135
  );
4136
+ this.embeddingService.reportHookFailed(
4137
+ { operation: "importDump", trigger: "call" },
4138
+ entityId,
4139
+ fact.id
4140
+ );
3877
4141
  }
3878
4142
  }
3879
4143
  }
@@ -3890,6 +4154,11 @@ var ImportExportService = class {
3890
4154
  `[WikiMemory] onEmbeddingPersisted(vector=null) hook failed for soft-deleted fact ${factId}:`,
3891
4155
  hookErr
3892
4156
  );
4157
+ this.embeddingService.reportHookFailed(
4158
+ { operation: "importDump", trigger: "call" },
4159
+ entityId,
4160
+ factId
4161
+ );
3893
4162
  }
3894
4163
  }
3895
4164
  }
@@ -4052,7 +4321,7 @@ var EmbeddingService = class {
4052
4321
  });
4053
4322
  }
4054
4323
  }
4055
- async tryEmbedFact(fact) {
4324
+ async tryEmbedFact(fact, ctx) {
4056
4325
  const embedFn = this.options.llmProvider.embed;
4057
4326
  if (typeof embedFn !== "function") return { ok: false, kind: "no_provider" };
4058
4327
  let tagsStr;
@@ -4074,6 +4343,7 @@ var EmbeddingService = class {
4074
4343
  const vector = await embedFn(text);
4075
4344
  if (vector.length === 0 || !vector.every((v) => typeof v === "number" && isFinite(v))) {
4076
4345
  console.warn(`[WikiMemory] embedFact: embed() returned an invalid vector for ${fact.id}; skipping.`);
4346
+ this.reportEmbed(ctx, fact, "embedding_failed", "invalid_vector");
4077
4347
  await this.markFailure(fact.id, "invalid_vector");
4078
4348
  return { ok: false, kind: "invalid_vector" };
4079
4349
  }
@@ -4087,11 +4357,13 @@ var EmbeddingService = class {
4087
4357
  }
4088
4358
  if (hasNonFinite) {
4089
4359
  console.warn(`[WikiMemory] embedFact: embed() returned values that overflow float32 for ${fact.id}; skipping.`);
4360
+ this.reportEmbed(ctx, fact, "embedding_failed", "float32_overflow");
4090
4361
  await this.markFailure(fact.id, "float32_overflow");
4091
4362
  return { ok: false, kind: "float32_overflow" };
4092
4363
  }
4093
4364
  } catch (err) {
4094
4365
  console.warn(`[WikiMemory] embedFact failed for ${fact.id}:`, err);
4366
+ this.reportEmbed(ctx, fact, "embedding_failed", "embed_threw");
4095
4367
  await this.markFailure(fact.id, "provider_error");
4096
4368
  return { ok: false, kind: "provider_error" };
4097
4369
  }
@@ -4101,15 +4373,31 @@ var EmbeddingService = class {
4101
4373
  await this.entryRepo.updateEmbeddingBlob(fact.id, blob);
4102
4374
  } catch (err) {
4103
4375
  console.warn(`[WikiMemory] embedFact: persisting embedding failed for ${fact.id}:`, err);
4376
+ this.reportEmbed(ctx, fact, "embedding_failed", "persist_failed");
4104
4377
  return { ok: false, kind: "storage_error" };
4105
4378
  }
4106
4379
  try {
4107
4380
  await this.notifyEmbeddingPersisted(fact.entity_id, fact.id, float32Vector);
4108
4381
  } catch (hookErr) {
4109
4382
  console.warn(`[WikiMemory] onEmbeddingPersisted hook failed for ${fact.id}:`, hookErr);
4383
+ this.reportEmbed(ctx, fact, "hook_failed", "on_embedding_persisted");
4110
4384
  }
4111
4385
  return { ok: true, dimension: float32Vector.length };
4112
4386
  }
4387
+ reportEmbed(ctx, fact, code, reason) {
4388
+ if (!ctx) return;
4389
+ emitDiagnostic(this.options, {
4390
+ code,
4391
+ operation: ctx.operation,
4392
+ trigger: ctx.trigger,
4393
+ entityId: fact.entity_id,
4394
+ detail: { factId: fact.id, reason }
4395
+ });
4396
+ }
4397
+ /** For callers that invoke `notifyEmbeddingPersisted` directly and catch its failure. */
4398
+ reportHookFailed(ctx, entityId, factId) {
4399
+ this.reportEmbed(ctx, { id: factId, entity_id: entityId }, "hook_failed", "on_embedding_persisted");
4400
+ }
4113
4401
  /** Marker writes must never fail the caller. Only marker-eligible kinds reach here. */
4114
4402
  async markFailure(id, kind) {
4115
4403
  try {
@@ -4123,8 +4411,8 @@ var EmbeddingService = class {
4123
4411
  * ImportExportService branch on `if (!embedded)`; a discriminated result
4124
4412
  * object is always truthy and would silently disable that branch.
4125
4413
  */
4126
- async embedFact(fact) {
4127
- const result = await this.tryEmbedFact(fact);
4414
+ async embedFact(fact, ctx) {
4415
+ const result = await this.tryEmbedFact(fact, ctx);
4128
4416
  return result.ok;
4129
4417
  }
4130
4418
  async notifyEmbeddingPersisted(entityId, factId, vector) {
@@ -4924,7 +5212,11 @@ var WriteService = class {
4924
5212
  if (shouldRunLibrarian) {
4925
5213
  try {
4926
5214
  this.jobManager.acquireLock("librarian", entityId);
4927
- this.runLibrarianThenMaybeHeal(entityId, librarianCount, prevMemoryCheckpoint).catch(console.error).finally(() => {
5215
+ const stage = { job: "librarian" };
5216
+ this.runLibrarianThenMaybeHeal(entityId, librarianCount, prevMemoryCheckpoint, stage).catch((err) => {
5217
+ console.error(err);
5218
+ this.reportBackgroundFailure(entityId, stage.job);
5219
+ }).finally(() => {
4928
5220
  this.jobManager.releaseLock("librarian", entityId);
4929
5221
  });
4930
5222
  } catch (e) {
@@ -4932,17 +5224,21 @@ var WriteService = class {
4932
5224
  await this.metadataRepo.updateCheckpoint(entityId, { memory: prevMemoryCheckpoint }, this.db);
4933
5225
  }
4934
5226
  } else if (!this.jobManager.isBlocked("librarian", entityId)) {
4935
- this.maybeRunHeal(entityId, eventCount).catch(console.error);
5227
+ this.maybeRunHeal(entityId, eventCount).catch((err) => {
5228
+ console.error(err);
5229
+ this.reportBackgroundFailure(entityId, "heal");
5230
+ });
4936
5231
  }
4937
5232
  }
4938
- async runLibrarianThenMaybeHeal(entityId, currentEventCount, prevCheckpoint) {
5233
+ async runLibrarianThenMaybeHeal(entityId, currentEventCount, prevCheckpoint, stage) {
4939
5234
  try {
4940
- await this.maintenanceService.doRunLibrarian(entityId);
5235
+ await this.maintenanceService.doRunLibrarian(entityId, void 0, "auto");
4941
5236
  await this.metadataRepo.updateCheckpoint(entityId, { memory: currentEventCount }, this.db);
4942
5237
  } catch (e) {
4943
5238
  await this.metadataRepo.updateCheckpoint(entityId, { memory: prevCheckpoint }, this.db);
4944
5239
  throw e;
4945
5240
  }
5241
+ stage.job = "heal";
4946
5242
  await this.maybeRunHeal(entityId, currentEventCount);
4947
5243
  }
4948
5244
  /**
@@ -4958,7 +5254,7 @@ var WriteService = class {
4958
5254
  const shouldRunHeal = currentEventCount - healCheckpoint >= autoHealThreshold;
4959
5255
  if (shouldRunHeal && this.jobManager.tryAcquireAutoHealLock(entityId)) {
4960
5256
  try {
4961
- const result = await this.maintenanceService.doRunHeal(entityId);
5257
+ const result = await this.maintenanceService.doRunHeal(entityId, { trigger: "auto" });
4962
5258
  if (result.remaining === 0) {
4963
5259
  await this.metadataRepo.updateCheckpoint(entityId, { heal: currentEventCount }, this.db);
4964
5260
  }
@@ -4967,8 +5263,17 @@ var WriteService = class {
4967
5263
  }
4968
5264
  }
4969
5265
  }
5266
+ reportBackgroundFailure(entityId, job) {
5267
+ emitDiagnostic(this.options, {
5268
+ code: "background_job_failed",
5269
+ operation: job,
5270
+ trigger: "auto",
5271
+ entityId,
5272
+ detail: { reason: "unhandled_rejection" }
5273
+ });
5274
+ }
4970
5275
  };
4971
5276
 
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
5277
+ 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, 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 };
5278
+ //# sourceMappingURL=chunk-IAYO63DQ.mjs.map
5279
+ //# sourceMappingURL=chunk-IAYO63DQ.mjs.map