@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.
package/dist/index.js CHANGED
@@ -2768,20 +2768,27 @@ function mergeOntologyUpdates(current, updates) {
2768
2768
  function validateInlineEdges(sourceType, _targetType, edges, manifest, opts) {
2769
2769
  const strict = opts?.strict === true;
2770
2770
  const entityId = opts?.entityId ?? "";
2771
+ const drops = opts?.drops;
2772
+ const drop = (reason, edgeType) => {
2773
+ drops?.push({ reason, sourceId: null, edgeType, sourceNodeType: sourceType, targetNodeType: null });
2774
+ };
2771
2775
  if (!Array.isArray(edges)) {
2772
2776
  if (strict) throw new WikiStrictOntologyViolation(entityId, "edge", "");
2777
+ drop("invalid_shape", null);
2773
2778
  return [];
2774
2779
  }
2775
2780
  const valid = [];
2776
2781
  for (const edge of edges) {
2777
2782
  if (typeof edge?.edge_type !== "string" || typeof edge?.target_title !== "string") {
2778
2783
  if (strict) throw new WikiStrictOntologyViolation(entityId, "edge", String(edge?.edge_type ?? ""));
2784
+ drop("invalid_shape", typeof edge?.edge_type === "string" ? edge.edge_type : null);
2779
2785
  continue;
2780
2786
  }
2781
2787
  const defs = resolveEdgeDefinitions(edge.edge_type, manifest);
2782
2788
  const match = defs.find((d) => typeSatisfies(d.source_type, sourceType, manifest));
2783
2789
  if (!match) {
2784
2790
  if (strict) throw new WikiStrictOntologyViolation(entityId, "edge", edge.edge_type);
2791
+ drop("type_not_in_manifest", edge.edge_type);
2785
2792
  continue;
2786
2793
  }
2787
2794
  valid.push({ edge_type: match.type, target_title: edge.target_title });
@@ -4129,6 +4136,17 @@ function validateTask(task) {
4129
4136
  priority
4130
4137
  };
4131
4138
  }
4139
+ function factRejectionReason(raw) {
4140
+ const fact = raw;
4141
+ if (typeof fact?.title !== "string" || typeof fact?.body !== "string") return "invalid_shape";
4142
+ if (!clip(fact.title, 80)) return "missing_title";
4143
+ return "missing_body";
4144
+ }
4145
+ function taskRejectionReason(raw) {
4146
+ const task = raw;
4147
+ if (typeof task?.description !== "string") return "invalid_shape";
4148
+ return "missing_description";
4149
+ }
4132
4150
  function normalizeSourceRef(value) {
4133
4151
  if (typeof value !== "string") return null;
4134
4152
  const cleaned = value.replace(/[^A-Za-z0-9._\- ]/g, "").trim().slice(0, 255);
@@ -4148,6 +4166,104 @@ function jaccardScore(a, b) {
4148
4166
  return intersection.size / union.size;
4149
4167
  }
4150
4168
 
4169
+ // src/utils/diagnostics.ts
4170
+ var SEVERITY = {
4171
+ ingest_chunk_failed: "warn",
4172
+ fact_rejected: "warn",
4173
+ task_rejected: "warn",
4174
+ fact_deduplicated: "info",
4175
+ edge_dropped: "warn",
4176
+ embedding_failed: "warn",
4177
+ hook_failed: "warn",
4178
+ background_job_failed: "error",
4179
+ heal_skipped: "warn",
4180
+ grounding_missing: "warn",
4181
+ grounding_failed: "warn",
4182
+ classification_low_confidence: "info",
4183
+ classification_invalid: "warn"
4184
+ };
4185
+ var MESSAGE = {
4186
+ ingest_chunk_failed: "One or more document chunks failed to parse or generate.",
4187
+ fact_rejected: "An extracted fact failed validation and was not written.",
4188
+ task_rejected: "An extracted task failed validation and was not written.",
4189
+ fact_deduplicated: "An extracted fact duplicated an existing fact and was not written.",
4190
+ edge_dropped: "An extracted edge could not be resolved against the ontology and was not written.",
4191
+ embedding_failed: "A fact could not be embedded.",
4192
+ hook_failed: "A host hook threw or rejected.",
4193
+ background_job_failed: "A background maintenance job failed.",
4194
+ heal_skipped: "Heal skipped a candidate fact.",
4195
+ grounding_missing: "A fact carried no qualifying evidence and was stored as a draft.",
4196
+ grounding_failed: "A fact carried evidence not found in its source and was stored as a draft.",
4197
+ classification_low_confidence: "A classifier answer was below the confidence threshold and was not applied.",
4198
+ classification_invalid: "A classifier answer was invalid and was not applied."
4199
+ };
4200
+ var warnedNonFunction = /* @__PURE__ */ new WeakSet();
4201
+ function copyDetail(detail) {
4202
+ const copy = { ...detail };
4203
+ if (detail.chunkIndexes) copy.chunkIndexes = detail.chunkIndexes.slice();
4204
+ return copy;
4205
+ }
4206
+ function emitDiagnostic(options, input) {
4207
+ const hook = options.onDiagnostic;
4208
+ if (hook === void 0 || hook === null) return;
4209
+ if (typeof hook !== "function") {
4210
+ if (!warnedNonFunction.has(options)) {
4211
+ warnedNonFunction.add(options);
4212
+ console.warn("[WikiMemory] onDiagnostic is not a function; diagnostics are disabled.");
4213
+ }
4214
+ return;
4215
+ }
4216
+ const diagnostic = {
4217
+ code: input.code,
4218
+ severity: SEVERITY[input.code],
4219
+ operation: input.operation,
4220
+ trigger: input.trigger,
4221
+ entityId: input.entityId,
4222
+ at: Date.now(),
4223
+ message: MESSAGE[input.code],
4224
+ ...input.detail ? { detail: copyDetail(input.detail) } : {}
4225
+ };
4226
+ try {
4227
+ const returned = hook(diagnostic);
4228
+ if (returned !== void 0) {
4229
+ Promise.resolve(returned).catch((err) => {
4230
+ console.warn("[WikiMemory] onDiagnostic hook rejected:", err);
4231
+ });
4232
+ }
4233
+ } catch (err) {
4234
+ console.warn("[WikiMemory] onDiagnostic hook threw:", err);
4235
+ }
4236
+ }
4237
+ var DiagnosticBuffer = class {
4238
+ constructor() {
4239
+ this.items = [];
4240
+ }
4241
+ push(input) {
4242
+ this.items.push(input);
4243
+ }
4244
+ get size() {
4245
+ return this.items.length;
4246
+ }
4247
+ flush(options) {
4248
+ const pending = this.items;
4249
+ this.items = [];
4250
+ for (const input of pending) emitDiagnostic(options, input);
4251
+ }
4252
+ discard() {
4253
+ this.items = [];
4254
+ }
4255
+ };
4256
+ function edgeDropDiagnostic(drop, ctx) {
4257
+ const detail = { reason: drop.reason };
4258
+ const factId = ctx.factId ?? drop.sourceId;
4259
+ if (factId) detail.factId = factId;
4260
+ if (drop.edgeType) detail.edgeType = drop.edgeType;
4261
+ if (drop.sourceNodeType) detail.sourceNodeType = drop.sourceNodeType;
4262
+ if (drop.targetNodeType) detail.targetNodeType = drop.targetNodeType;
4263
+ if (ctx.sourceRef) detail.sourceRef = ctx.sourceRef;
4264
+ return { code: "edge_dropped", operation: ctx.operation, trigger: ctx.trigger, entityId: ctx.entityId, detail };
4265
+ }
4266
+
4151
4267
  // src/prompts.ts
4152
4268
  var LIBRARIAN_SYSTEM_PROMPT = `You are a knowledge extraction agent. Your job is to analyze recent episodic events and extract stable facts and actionable tasks about the user or entity.
4153
4269
  Return ONLY a valid JSON object matching this schema:
@@ -4435,9 +4551,24 @@ var IngestionService = class {
4435
4551
  try {
4436
4552
  const responseText = await this.options.llmProvider.generateText({ systemPrompt, userPrompt });
4437
4553
  const result2 = parseJsonResponse(responseText);
4554
+ const rawFacts = Array.isArray(result2.facts) ? result2.facts : [];
4555
+ const facts = [];
4556
+ const itemIndexes = [];
4557
+ const rejected = [];
4558
+ rawFacts.forEach((raw, itemIndex) => {
4559
+ const valid = validateFact(raw);
4560
+ if (valid) {
4561
+ facts.push(valid);
4562
+ itemIndexes.push(itemIndex);
4563
+ } else {
4564
+ rejected.push({ itemIndex, reason: factRejectionReason(raw) });
4565
+ }
4566
+ });
4438
4567
  return {
4439
4568
  status: "ok",
4440
- facts: (Array.isArray(result2.facts) ? result2.facts : []).map(validateFact).filter((f) => f !== null),
4569
+ facts,
4570
+ itemIndexes,
4571
+ rejected,
4441
4572
  ontology_updates: result2.ontology_updates
4442
4573
  };
4443
4574
  } catch (e) {
@@ -4482,23 +4613,48 @@ var IngestionService = class {
4482
4613
  const failures = [];
4483
4614
  const seen = /* @__PURE__ */ new Set();
4484
4615
  const orderedChunkFacts = [];
4485
- for (const slot of chunkResults) {
4616
+ const diagBuffer = new DiagnosticBuffer();
4617
+ const diagBase = { entityId, operation: "ingest", trigger: "call" };
4618
+ for (const [chunkIndex, slot] of chunkResults.entries()) {
4486
4619
  if (slot.status === "failed") {
4487
4620
  failedChunks++;
4488
4621
  failures.push(slot.error);
4489
4622
  continue;
4490
4623
  }
4491
4624
  ingestedChunks++;
4625
+ for (const r of slot.rejected) {
4626
+ diagBuffer.push({ ...diagBase, code: "fact_rejected", detail: { sourceRef, chunkIndex, itemIndex: r.itemIndex, reason: r.reason } });
4627
+ }
4492
4628
  const dedupedFacts = [];
4493
- for (const fact of slot.facts) {
4629
+ slot.facts.forEach((fact, k) => {
4494
4630
  const normalizedTitle = normalizeTitleKey(fact.title);
4495
4631
  if (!seen.has(normalizedTitle)) {
4496
4632
  seen.add(normalizedTitle);
4497
4633
  dedupedFacts.push(fact);
4634
+ } else {
4635
+ diagBuffer.push({
4636
+ ...diagBase,
4637
+ code: "fact_deduplicated",
4638
+ detail: { sourceRef, chunkIndex, itemIndex: slot.itemIndexes[k], reason: "exact_title" }
4639
+ });
4498
4640
  }
4499
- }
4641
+ });
4500
4642
  orderedChunkFacts.push({ facts: dedupedFacts, ontology_updates: slot.ontology_updates });
4501
4643
  }
4644
+ for (const reason of ["parse", "llm"]) {
4645
+ const ofReason = failures.filter((f) => f.source === reason);
4646
+ if (ofReason.length === 0) continue;
4647
+ diagBuffer.push({
4648
+ ...diagBase,
4649
+ code: "ingest_chunk_failed",
4650
+ detail: {
4651
+ sourceRef,
4652
+ reason,
4653
+ count: ofReason.length,
4654
+ chunkIndexes: ofReason.map((f) => f.chunkIndex).sort((a, b) => a - b).slice(0, 20)
4655
+ }
4656
+ });
4657
+ }
4502
4658
  if (failedChunks === chunks.length) {
4503
4659
  throw new WikiIngestEmptyError({
4504
4660
  parseFailures: failures,
@@ -4511,7 +4667,7 @@ var IngestionService = class {
4511
4667
  try {
4512
4668
  if (failedChunks === 0) {
4513
4669
  const fullResult = await this.db.withTransactionAsync(async (tx) => {
4514
- return await this.runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx);
4670
+ return await this.runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx, diagBuffer);
4515
4671
  });
4516
4672
  deletedSourceFactIds.push(...fullResult.deletedSourceFactIds);
4517
4673
  insertedFacts.push(...fullResult.insertedFacts);
@@ -4519,7 +4675,7 @@ var IngestionService = class {
4519
4675
  const partialResult = await this.db.withTransactionAsync(async (tx) => {
4520
4676
  const flat = [];
4521
4677
  for (const slot of orderedChunkFacts) flat.push(...slot.facts);
4522
- return await this.appendPartialFacts(entityId, sourceRef, flat, tx);
4678
+ return await this.appendPartialFacts(entityId, sourceRef, flat, tx, diagBuffer);
4523
4679
  });
4524
4680
  insertedFacts.push(...partialResult.insertedDescriptors);
4525
4681
  }
@@ -4533,6 +4689,7 @@ var IngestionService = class {
4533
4689
  }
4534
4690
  return zeroChunkResult(canonical);
4535
4691
  }
4692
+ diagBuffer.flush(this.options);
4536
4693
  await this.searchService.sync(entityId);
4537
4694
  if (failedChunks === 0) {
4538
4695
  const uniqueDeletedSourceFactIds = Array.from(new Set(deletedSourceFactIds));
@@ -4541,11 +4698,18 @@ var IngestionService = class {
4541
4698
  await this.embeddingService.notifyEmbeddingPersisted(entityId, factId, null);
4542
4699
  } catch (hookErr) {
4543
4700
  console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during ingest for ${factId}:`, hookErr);
4701
+ emitDiagnostic(this.options, {
4702
+ entityId,
4703
+ operation: "ingest",
4704
+ trigger: "call",
4705
+ code: "hook_failed",
4706
+ detail: { factId, reason: "on_embedding_persisted" }
4707
+ });
4544
4708
  }
4545
4709
  }
4546
4710
  }
4547
4711
  for (const fact of insertedFacts) {
4548
- await this.embeddingService.embedFact(fact);
4712
+ await this.embeddingService.embedFact(fact, { operation: "ingest", trigger: "call" });
4549
4713
  }
4550
4714
  this.searchService.evictCache(entityId);
4551
4715
  const result = {
@@ -4691,6 +4855,19 @@ var IngestionService = class {
4691
4855
  const match = candidates[0];
4692
4856
  if (!match) {
4693
4857
  if (strictEffective) throw new WikiStrictOntologyViolation(entityId, "edge", edge.type);
4858
+ opts?.diag?.buffer.push({
4859
+ entityId,
4860
+ operation: opts.diag.operation,
4861
+ trigger: "call",
4862
+ code: "edge_dropped",
4863
+ detail: {
4864
+ reason: "manifest_violation",
4865
+ factId: edge.sourceId,
4866
+ edgeType: edge.type,
4867
+ ...sourceType ? { sourceNodeType: sourceType } : {},
4868
+ sourceRef: params.sourceRef
4869
+ }
4870
+ });
4694
4871
  continue;
4695
4872
  }
4696
4873
  validEdges.push({
@@ -4731,7 +4908,7 @@ var IngestionService = class {
4731
4908
  * On the partial path the caller never invokes this method; the empty
4732
4909
  * arrays stay empty.
4733
4910
  */
4734
- async runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx) {
4911
+ async runFullUpsertGraph(entityId, sourceRef, sourceHash, orderedChunkFacts, tx, diagBuffer) {
4735
4912
  const deletedSourceFactIds = [];
4736
4913
  const insertedFacts = [];
4737
4914
  deletedSourceFactIds.push(...await this.entryRepo.findIdsBySource(entityId, sourceRef, null, tx, false));
@@ -4756,8 +4933,12 @@ var IngestionService = class {
4756
4933
  }
4757
4934
  for (const fact of facts) {
4758
4935
  const ontologyFact = fact;
4759
- const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false }) ?? { okf_type: null, edges: [] };
4760
4936
  const id = generateId("fact_");
4937
+ const validationDrops = [];
4938
+ const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false, drops: validationDrops }) ?? { okf_type: null, edges: [] };
4939
+ for (const drop of validationDrops) {
4940
+ diagBuffer.push(edgeDropDiagnostic(drop, { entityId, operation: "ingest", trigger: "call", sourceRef, factId: id }));
4941
+ }
4761
4942
  hostNodes.push({
4762
4943
  id,
4763
4944
  type: ontologyFact.okf_type ?? "",
@@ -4782,6 +4963,7 @@ var IngestionService = class {
4782
4963
  }
4783
4964
  const hostEdges = [];
4784
4965
  for (const req of rawEdgeRequests) {
4966
+ const resolveDrops = [];
4785
4967
  const resolved = this.ontologyService?.resolveEdges(
4786
4968
  entityId,
4787
4969
  req.sourceId,
@@ -4789,8 +4971,12 @@ var IngestionService = class {
4789
4971
  req.edges,
4790
4972
  manifest,
4791
4973
  titleIndex,
4792
- now
4974
+ now,
4975
+ resolveDrops
4793
4976
  ) ?? [];
4977
+ for (const drop of resolveDrops) {
4978
+ diagBuffer.push(edgeDropDiagnostic(drop, { entityId, operation: "ingest", trigger: "call", sourceRef }));
4979
+ }
4794
4980
  for (const e of resolved) {
4795
4981
  hostEdges.push({ type: e.edge_type, sourceId: e.source_id, targetId: e.target_id });
4796
4982
  }
@@ -4799,7 +4985,7 @@ var IngestionService = class {
4799
4985
  entityId,
4800
4986
  { sourceRef, sourceHash, nodes: hostNodes, edges: hostEdges },
4801
4987
  tx,
4802
- { strict: false }
4988
+ { strict: false, diag: { buffer: diagBuffer, operation: "ingest" } }
4803
4989
  );
4804
4990
  return { deletedSourceFactIds, insertedFacts };
4805
4991
  }
@@ -4823,7 +5009,7 @@ var IngestionService = class {
4823
5009
  * Runs INSIDE the caller's `tx`. Does not open a nested transaction.
4824
5010
  * Returns `{ inserted, skippedDuplicate }` for observability.
4825
5011
  */
4826
- async appendPartialFacts(entityId, sourceRef, dedupedFacts, tx) {
5012
+ async appendPartialFacts(entityId, sourceRef, dedupedFacts, tx, diagBuffer) {
4827
5013
  const liveIds = await this.entryRepo.findIdsBySource(entityId, sourceRef, null, tx, false);
4828
5014
  const liveFacts = liveIds.length === 0 ? [] : await this.entryRepo.findByIds(liveIds, void 0, tx);
4829
5015
  const liveTitles = new Set(liveFacts.map((f) => normalizeTitleKey(f.title)));
@@ -4834,6 +5020,13 @@ var IngestionService = class {
4834
5020
  for (const fact of dedupedFacts) {
4835
5021
  const normalizedTitle = normalizeTitleKey(fact.title);
4836
5022
  if (liveTitles.has(normalizedTitle)) {
5023
+ diagBuffer.push({
5024
+ entityId,
5025
+ operation: "ingest",
5026
+ trigger: "call",
5027
+ code: "fact_deduplicated",
5028
+ detail: { sourceRef, reason: "exact_title" }
5029
+ });
4837
5030
  skippedDuplicate++;
4838
5031
  continue;
4839
5032
  }
@@ -5234,7 +5427,7 @@ var MaintenanceService = class {
5234
5427
  permanentlyFailed++;
5235
5428
  continue;
5236
5429
  }
5237
- const result = await this.embeddingService.tryEmbedFact(row);
5430
+ const result = await this.embeddingService.tryEmbedFact(row, { operation: "reembed", trigger: "call" });
5238
5431
  if (result.ok) embedded++;
5239
5432
  else failed++;
5240
5433
  }
@@ -5355,7 +5548,7 @@ var MaintenanceService = class {
5355
5548
  return { deleted: { entries, tasks: 0 } };
5356
5549
  }
5357
5550
  /** Core librarian pass (locks handled by {@link runLibrarian}). Package-internal orchestration hook. */
5358
- async doRunLibrarian(entityId, promptOverride) {
5551
+ async doRunLibrarian(entityId, promptOverride, trigger = "call") {
5359
5552
  const events = await this.eventRepo.getRecent(entityId, 50);
5360
5553
  const currentFactsRows = await this.entryRepo.findRecentByEntityId(entityId, 100);
5361
5554
  const currentFacts = currentFactsRows.map((f) => {
@@ -5377,8 +5570,25 @@ var MaintenanceService = class {
5377
5570
  const facts = Array.isArray(result.facts) ? result.facts : [];
5378
5571
  const tasks = Array.isArray(result.tasks) ? result.tasks : [];
5379
5572
  const ontologyUpdates = result.ontology_updates;
5380
- const validFacts = facts.map(validateFact).filter((f) => f !== null);
5381
- const validTasks = tasks.map(validateTask).filter((t) => t !== null);
5573
+ const diagBuffer = new DiagnosticBuffer();
5574
+ const diagBase = { entityId, operation: "librarian", trigger };
5575
+ const validFacts = [];
5576
+ const validFactItemIndexes = [];
5577
+ facts.forEach((raw, itemIndex) => {
5578
+ const valid = validateFact(raw);
5579
+ if (valid) {
5580
+ validFacts.push(valid);
5581
+ validFactItemIndexes.push(itemIndex);
5582
+ } else {
5583
+ diagBuffer.push({ ...diagBase, code: "fact_rejected", detail: { itemIndex, reason: factRejectionReason(raw) } });
5584
+ }
5585
+ });
5586
+ const validTasks = [];
5587
+ tasks.forEach((raw, itemIndex) => {
5588
+ const valid = validateTask(raw);
5589
+ if (valid) validTasks.push(valid);
5590
+ else diagBuffer.push({ ...diagBase, code: "task_rejected", detail: { itemIndex, reason: taskRejectionReason(raw) } });
5591
+ });
5382
5592
  const now = Date.now();
5383
5593
  const insertedFacts = [];
5384
5594
  await this.db.withTransactionAsync(async (tx) => {
@@ -5395,7 +5605,7 @@ var MaintenanceService = class {
5395
5605
  }
5396
5606
  const factsForDedupe = await this.entryRepo.findRecentByEntityId(entityId, 100, tx);
5397
5607
  const pendingEdges = [];
5398
- for (const fact of validFacts) {
5608
+ for (const [k, fact] of validFacts.entries()) {
5399
5609
  const newTokens = titleTokens(fact.title);
5400
5610
  let skip = false;
5401
5611
  if (newTokens.size >= MIN_TOKENS_TO_QUALIFY) {
@@ -5410,10 +5620,15 @@ var MaintenanceService = class {
5410
5620
  }
5411
5621
  }
5412
5622
  }
5413
- if (skip) continue;
5623
+ if (skip) {
5624
+ diagBuffer.push({ ...diagBase, code: "fact_deduplicated", detail: { itemIndex: validFactItemIndexes[k], reason: "fuzzy_title" } });
5625
+ continue;
5626
+ }
5414
5627
  const ontologyFact = fact;
5415
- const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false }) ?? { okf_type: null, edges: [] };
5416
5628
  const id = generateId("fact_");
5629
+ const validationDrops = [];
5630
+ const normalized = this.ontologyService?.validateAndNormalizeFact(ontologyFact, manifest, { strict: false, drops: validationDrops }) ?? { okf_type: null, edges: [] };
5631
+ for (const drop of validationDrops) diagBuffer.push(edgeDropDiagnostic(drop, { ...diagBase, factId: id }));
5417
5632
  const factObj = {
5418
5633
  id,
5419
5634
  entity_id: entityId,
@@ -5440,6 +5655,7 @@ var MaintenanceService = class {
5440
5655
  }
5441
5656
  }
5442
5657
  for (const item of pendingEdges) {
5658
+ const resolveDrops = [];
5443
5659
  await this.ontologyService?.resolveAndPersistEdges(
5444
5660
  entityId,
5445
5661
  item.sourceId,
@@ -5448,8 +5664,10 @@ var MaintenanceService = class {
5448
5664
  manifest,
5449
5665
  titleIndex,
5450
5666
  tx,
5451
- now
5667
+ now,
5668
+ resolveDrops
5452
5669
  );
5670
+ for (const drop of resolveDrops) diagBuffer.push(edgeDropDiagnostic(drop, diagBase));
5453
5671
  }
5454
5672
  for (const task of validTasks) {
5455
5673
  const id = generateId("task_");
@@ -5467,9 +5685,10 @@ var MaintenanceService = class {
5467
5685
  await this.taskRepo.upsert(taskObj, tx);
5468
5686
  }
5469
5687
  });
5688
+ diagBuffer.flush(this.options);
5470
5689
  await this.searchService.sync(entityId);
5471
5690
  for (const fact of insertedFacts) {
5472
- await this.embeddingService.embedFact(fact);
5691
+ await this.embeddingService.embedFact(fact, { operation: "librarian", trigger });
5473
5692
  }
5474
5693
  this.searchService.evictCache(entityId);
5475
5694
  }
@@ -5483,6 +5702,8 @@ var MaintenanceService = class {
5483
5702
  async doRunHeal(entityId, options) {
5484
5703
  const promptOverride = options?.promptOverride;
5485
5704
  const batchSize = options?.batchSize ?? HEAL_BATCH_SIZE;
5705
+ const trigger = options?.trigger ?? "call";
5706
+ const diagBase = { entityId, operation: "heal", trigger };
5486
5707
  if (!Number.isInteger(batchSize) || batchSize < 1) {
5487
5708
  throw new Error("Invalid batchSize: must be an integer >= 1");
5488
5709
  }
@@ -5518,6 +5739,7 @@ var MaintenanceService = class {
5518
5739
  await this.embeddingService.notifyEmbeddingPersisted(entityId, factId, null);
5519
5740
  } catch (hookErr) {
5520
5741
  console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during heal orphan pass for ${factId}:`, hookErr);
5742
+ emitDiagnostic(this.options, { ...diagBase, code: "hook_failed", detail: { factId, reason: "on_embedding_persisted" } });
5521
5743
  }
5522
5744
  }
5523
5745
  const healCandidates = await this.entryRepo.findHealCandidatesByEntityId(entityId, batchSize, recheckCutoff);
@@ -5590,22 +5812,38 @@ var MaintenanceService = class {
5590
5812
  const safeDowngradedSet = /* @__PURE__ */ new Set();
5591
5813
  const safeDeletedSet = /* @__PURE__ */ new Set();
5592
5814
  const newFacts = [];
5815
+ const newFactItemIndexes = [];
5593
5816
  for (const batchResult of outcome.results) {
5594
5817
  const mutableIds = new Set(batchResult.batch.map((f) => f.id));
5595
5818
  for (const id of batchResult.downgraded) if (mutableIds.has(id)) safeDowngradedSet.add(id);
5596
5819
  for (const id of batchResult.deleted) if (mutableIds.has(id)) safeDeletedSet.add(id);
5597
- newFacts.push(...batchResult.newFacts);
5820
+ batchResult.newFacts.forEach((raw, itemIndex) => {
5821
+ newFacts.push(raw);
5822
+ newFactItemIndexes.push(itemIndex);
5823
+ });
5598
5824
  }
5599
5825
  const safeDowngraded = Array.from(safeDowngradedSet);
5600
5826
  const safeDeleted = Array.from(safeDeletedSet);
5601
- const validNewFacts = newFacts.map(validateFact).filter((f) => f !== null);
5827
+ const diagBuffer = new DiagnosticBuffer();
5828
+ const validNewFacts = [];
5829
+ const validNewFactItemIndexes = [];
5830
+ newFacts.forEach((raw, k) => {
5831
+ const itemIndex = newFactItemIndexes[k];
5832
+ const valid = validateFact(raw);
5833
+ if (valid) {
5834
+ validNewFacts.push(valid);
5835
+ validNewFactItemIndexes.push(itemIndex);
5836
+ } else {
5837
+ diagBuffer.push({ ...diagBase, code: "fact_rejected", detail: { itemIndex, reason: factRejectionReason(raw) } });
5838
+ }
5839
+ });
5602
5840
  const insertedFacts = [];
5603
5841
  const uniqueDeletedFactIds = Array.from(new Set(safeDeleted));
5604
5842
  await this.db.withTransactionAsync(async (tx) => {
5605
5843
  await this.entryRepo.downgradeByIds(safeDowngraded, entityId, tx);
5606
5844
  await this.entryRepo.softDeleteByIds(safeDeleted, entityId, tx);
5607
5845
  const healFactsForDedupe = await this.entryRepo.findInferredTitlesByEntityId(entityId, tx);
5608
- for (const fact of validNewFacts) {
5846
+ for (const [k, fact] of validNewFacts.entries()) {
5609
5847
  const newTokens = titleTokens(fact.title);
5610
5848
  let skip = false;
5611
5849
  if (newTokens.size >= MIN_TOKENS_TO_QUALIFY) {
@@ -5619,7 +5857,10 @@ var MaintenanceService = class {
5619
5857
  }
5620
5858
  }
5621
5859
  }
5622
- if (skip) continue;
5860
+ if (skip) {
5861
+ diagBuffer.push({ ...diagBase, code: "fact_deduplicated", detail: { itemIndex: validNewFactItemIndexes[k], reason: "fuzzy_title" } });
5862
+ continue;
5863
+ }
5623
5864
  const id = generateId("fact_");
5624
5865
  const factObj = {
5625
5866
  id,
@@ -5654,16 +5895,21 @@ var MaintenanceService = class {
5654
5895
  tx
5655
5896
  );
5656
5897
  });
5898
+ for (const { item, reason } of outcome.skipped) {
5899
+ diagBuffer.push({ ...diagBase, code: "heal_skipped", detail: { factId: item.id, reason } });
5900
+ }
5901
+ diagBuffer.flush(this.options);
5657
5902
  await this.searchService.sync(entityId);
5658
5903
  for (const factId of uniqueDeletedFactIds) {
5659
5904
  try {
5660
5905
  await this.embeddingService.notifyEmbeddingPersisted(entityId, factId, null);
5661
5906
  } catch (hookErr) {
5662
5907
  console.warn(`[WikiMemory] onEmbeddingPersisted hook failed during heal for ${factId}:`, hookErr);
5908
+ emitDiagnostic(this.options, { ...diagBase, code: "hook_failed", detail: { factId, reason: "on_embedding_persisted" } });
5663
5909
  }
5664
5910
  }
5665
5911
  for (const fact of insertedFacts) {
5666
- await this.embeddingService.embedFact(fact);
5912
+ await this.embeddingService.embedFact(fact, { operation: "heal", trigger });
5667
5913
  }
5668
5914
  this.searchService.evictCache(entityId);
5669
5915
  const skippedIds = new Set(outcome.skipped.map(({ item }) => item.id));
@@ -5803,6 +6049,8 @@ var MaintenanceService = class {
5803
6049
  let failedValidation = 0;
5804
6050
  let edgesAdded = 0;
5805
6051
  let abortedOntologyOff = false;
6052
+ const diagBuffer = new DiagnosticBuffer();
6053
+ const diagBase = { entityId, operation: "ontologyBackfill", trigger: "call" };
5806
6054
  await this.db.withTransactionAsync(async (tx) => {
5807
6055
  let { mode: txMode, manifest } = await ontologyService.getEffectiveState(entityId, tx);
5808
6056
  if (txMode === "off") {
@@ -5827,6 +6075,7 @@ var MaintenanceService = class {
5827
6075
  continue;
5828
6076
  }
5829
6077
  applied.add(fact.id);
6078
+ const validationDrops = [];
5830
6079
  const normalized = ontologyService.validateAndNormalizeFact(
5831
6080
  {
5832
6081
  title: fact.title,
@@ -5837,8 +6086,11 @@ var MaintenanceService = class {
5837
6086
  edges: classification.edges
5838
6087
  },
5839
6088
  manifest,
5840
- { strict: false }
6089
+ { strict: false, drops: validationDrops }
5841
6090
  );
6091
+ for (const drop of validationDrops) {
6092
+ diagBuffer.push(edgeDropDiagnostic(drop, { ...diagBase, factId: fact.id }));
6093
+ }
5842
6094
  if (!normalized.okf_type) {
5843
6095
  failedValidation++;
5844
6096
  continue;
@@ -5852,7 +6104,8 @@ var MaintenanceService = class {
5852
6104
  }
5853
6105
  }
5854
6106
  for (const item of pendingEdges) {
5855
- edgesAdded += await ontologyService.resolveAndPersistEdges(
6107
+ const resolveDrops = [];
6108
+ const added = await ontologyService.resolveAndPersistEdges(
5856
6109
  entityId,
5857
6110
  item.sourceId,
5858
6111
  item.sourceType,
@@ -5860,11 +6113,17 @@ var MaintenanceService = class {
5860
6113
  manifest,
5861
6114
  titleIndex,
5862
6115
  tx,
5863
- now
6116
+ now,
6117
+ resolveDrops
5864
6118
  );
6119
+ edgesAdded += added;
6120
+ for (const drop of resolveDrops) {
6121
+ diagBuffer.push(edgeDropDiagnostic(drop, diagBase));
6122
+ }
5865
6123
  }
5866
6124
  await this.entryRepo.markOntologyChecked(batch.map((f) => f.id), entityId, now, tx);
5867
6125
  });
6126
+ diagBuffer.flush(this.options);
5868
6127
  return { typed, failedValidation, edgesAdded, abortedOntologyOff };
5869
6128
  }
5870
6129
  _validatePruneDuration(value, name) {
@@ -6266,7 +6525,7 @@ var ImportExportService = class {
6266
6525
  title: clipped?.title ?? fact.title,
6267
6526
  body: clipped?.body ?? fact.body,
6268
6527
  tags: Array.isArray(fact.tags) || typeof fact.tags === "string" ? fact.tags : []
6269
- });
6528
+ }, { operation: "importDump", trigger: "call" });
6270
6529
  if (!embedded) {
6271
6530
  await this.embeddingService.notifyEmbeddingPersisted(entityId, fact.id, null);
6272
6531
  }
@@ -6291,6 +6550,11 @@ var ImportExportService = class {
6291
6550
  `[WikiMemory] onEmbeddingPersisted hook failed for preserved-blob fact ${fact.id}:`,
6292
6551
  hookErr
6293
6552
  );
6553
+ this.embeddingService.reportHookFailed(
6554
+ { operation: "importDump", trigger: "call" },
6555
+ entityId,
6556
+ fact.id
6557
+ );
6294
6558
  }
6295
6559
  }
6296
6560
  }
@@ -6307,6 +6571,11 @@ var ImportExportService = class {
6307
6571
  `[WikiMemory] onEmbeddingPersisted(vector=null) hook failed for soft-deleted fact ${factId}:`,
6308
6572
  hookErr
6309
6573
  );
6574
+ this.embeddingService.reportHookFailed(
6575
+ { operation: "importDump", trigger: "call" },
6576
+ entityId,
6577
+ factId
6578
+ );
6310
6579
  }
6311
6580
  }
6312
6581
  }
@@ -6469,7 +6738,7 @@ var EmbeddingService = class {
6469
6738
  });
6470
6739
  }
6471
6740
  }
6472
- async tryEmbedFact(fact) {
6741
+ async tryEmbedFact(fact, ctx) {
6473
6742
  const embedFn = this.options.llmProvider.embed;
6474
6743
  if (typeof embedFn !== "function") return { ok: false, kind: "no_provider" };
6475
6744
  let tagsStr;
@@ -6491,6 +6760,7 @@ var EmbeddingService = class {
6491
6760
  const vector = await embedFn(text);
6492
6761
  if (vector.length === 0 || !vector.every((v) => typeof v === "number" && isFinite(v))) {
6493
6762
  console.warn(`[WikiMemory] embedFact: embed() returned an invalid vector for ${fact.id}; skipping.`);
6763
+ this.reportEmbed(ctx, fact, "embedding_failed", "invalid_vector");
6494
6764
  await this.markFailure(fact.id, "invalid_vector");
6495
6765
  return { ok: false, kind: "invalid_vector" };
6496
6766
  }
@@ -6504,11 +6774,13 @@ var EmbeddingService = class {
6504
6774
  }
6505
6775
  if (hasNonFinite) {
6506
6776
  console.warn(`[WikiMemory] embedFact: embed() returned values that overflow float32 for ${fact.id}; skipping.`);
6777
+ this.reportEmbed(ctx, fact, "embedding_failed", "float32_overflow");
6507
6778
  await this.markFailure(fact.id, "float32_overflow");
6508
6779
  return { ok: false, kind: "float32_overflow" };
6509
6780
  }
6510
6781
  } catch (err) {
6511
6782
  console.warn(`[WikiMemory] embedFact failed for ${fact.id}:`, err);
6783
+ this.reportEmbed(ctx, fact, "embedding_failed", "embed_threw");
6512
6784
  await this.markFailure(fact.id, "provider_error");
6513
6785
  return { ok: false, kind: "provider_error" };
6514
6786
  }
@@ -6518,15 +6790,31 @@ var EmbeddingService = class {
6518
6790
  await this.entryRepo.updateEmbeddingBlob(fact.id, blob);
6519
6791
  } catch (err) {
6520
6792
  console.warn(`[WikiMemory] embedFact: persisting embedding failed for ${fact.id}:`, err);
6793
+ this.reportEmbed(ctx, fact, "embedding_failed", "persist_failed");
6521
6794
  return { ok: false, kind: "storage_error" };
6522
6795
  }
6523
6796
  try {
6524
6797
  await this.notifyEmbeddingPersisted(fact.entity_id, fact.id, float32Vector);
6525
6798
  } catch (hookErr) {
6526
6799
  console.warn(`[WikiMemory] onEmbeddingPersisted hook failed for ${fact.id}:`, hookErr);
6800
+ this.reportEmbed(ctx, fact, "hook_failed", "on_embedding_persisted");
6527
6801
  }
6528
6802
  return { ok: true, dimension: float32Vector.length };
6529
6803
  }
6804
+ reportEmbed(ctx, fact, code, reason) {
6805
+ if (!ctx) return;
6806
+ emitDiagnostic(this.options, {
6807
+ code,
6808
+ operation: ctx.operation,
6809
+ trigger: ctx.trigger,
6810
+ entityId: fact.entity_id,
6811
+ detail: { factId: fact.id, reason }
6812
+ });
6813
+ }
6814
+ /** For callers that invoke `notifyEmbeddingPersisted` directly and catch its failure. */
6815
+ reportHookFailed(ctx, entityId, factId) {
6816
+ this.reportEmbed(ctx, { id: factId, entity_id: entityId }, "hook_failed", "on_embedding_persisted");
6817
+ }
6530
6818
  /** Marker writes must never fail the caller. Only marker-eligible kinds reach here. */
6531
6819
  async markFailure(id, kind) {
6532
6820
  try {
@@ -6540,8 +6828,8 @@ var EmbeddingService = class {
6540
6828
  * ImportExportService branch on `if (!embedded)`; a discriminated result
6541
6829
  * object is always truthy and would silently disable that branch.
6542
6830
  */
6543
- async embedFact(fact) {
6544
- const result = await this.tryEmbedFact(fact);
6831
+ async embedFact(fact, ctx) {
6832
+ const result = await this.tryEmbedFact(fact, ctx);
6545
6833
  return result.ok;
6546
6834
  }
6547
6835
  async notifyEmbeddingPersisted(entityId, factId, vector) {
@@ -7341,7 +7629,11 @@ var WriteService = class {
7341
7629
  if (shouldRunLibrarian) {
7342
7630
  try {
7343
7631
  this.jobManager.acquireLock("librarian", entityId);
7344
- this.runLibrarianThenMaybeHeal(entityId, librarianCount, prevMemoryCheckpoint).catch(console.error).finally(() => {
7632
+ const stage = { job: "librarian" };
7633
+ this.runLibrarianThenMaybeHeal(entityId, librarianCount, prevMemoryCheckpoint, stage).catch((err) => {
7634
+ console.error(err);
7635
+ this.reportBackgroundFailure(entityId, stage.job);
7636
+ }).finally(() => {
7345
7637
  this.jobManager.releaseLock("librarian", entityId);
7346
7638
  });
7347
7639
  } catch (e) {
@@ -7349,17 +7641,21 @@ var WriteService = class {
7349
7641
  await this.metadataRepo.updateCheckpoint(entityId, { memory: prevMemoryCheckpoint }, this.db);
7350
7642
  }
7351
7643
  } else if (!this.jobManager.isBlocked("librarian", entityId)) {
7352
- this.maybeRunHeal(entityId, eventCount).catch(console.error);
7644
+ this.maybeRunHeal(entityId, eventCount).catch((err) => {
7645
+ console.error(err);
7646
+ this.reportBackgroundFailure(entityId, "heal");
7647
+ });
7353
7648
  }
7354
7649
  }
7355
- async runLibrarianThenMaybeHeal(entityId, currentEventCount, prevCheckpoint) {
7650
+ async runLibrarianThenMaybeHeal(entityId, currentEventCount, prevCheckpoint, stage) {
7356
7651
  try {
7357
- await this.maintenanceService.doRunLibrarian(entityId);
7652
+ await this.maintenanceService.doRunLibrarian(entityId, void 0, "auto");
7358
7653
  await this.metadataRepo.updateCheckpoint(entityId, { memory: currentEventCount }, this.db);
7359
7654
  } catch (e) {
7360
7655
  await this.metadataRepo.updateCheckpoint(entityId, { memory: prevCheckpoint }, this.db);
7361
7656
  throw e;
7362
7657
  }
7658
+ stage.job = "heal";
7363
7659
  await this.maybeRunHeal(entityId, currentEventCount);
7364
7660
  }
7365
7661
  /**
@@ -7375,7 +7671,7 @@ var WriteService = class {
7375
7671
  const shouldRunHeal = currentEventCount - healCheckpoint >= autoHealThreshold;
7376
7672
  if (shouldRunHeal && this.jobManager.tryAcquireAutoHealLock(entityId)) {
7377
7673
  try {
7378
- const result = await this.maintenanceService.doRunHeal(entityId);
7674
+ const result = await this.maintenanceService.doRunHeal(entityId, { trigger: "auto" });
7379
7675
  if (result.remaining === 0) {
7380
7676
  await this.metadataRepo.updateCheckpoint(entityId, { heal: currentEventCount }, this.db);
7381
7677
  }
@@ -7384,6 +7680,15 @@ var WriteService = class {
7384
7680
  }
7385
7681
  }
7386
7682
  }
7683
+ reportBackgroundFailure(entityId, job) {
7684
+ emitDiagnostic(this.options, {
7685
+ code: "background_job_failed",
7686
+ operation: job,
7687
+ trigger: "auto",
7688
+ entityId,
7689
+ detail: { reason: "unhandled_rejection" }
7690
+ });
7691
+ }
7387
7692
  };
7388
7693
 
7389
7694
  // src/prompts/ontology.ts
@@ -7472,6 +7777,17 @@ var OntologyService = class {
7472
7777
  const canonical = resolveNodeType(rawType, manifest);
7473
7778
  if (!canonical) {
7474
7779
  if (strict) throw new WikiStrictOntologyViolation(opts?.entityId ?? "", "node", rawType);
7780
+ if (opts?.drops && Array.isArray(fact.edges)) {
7781
+ for (const edge of fact.edges) {
7782
+ opts.drops.push({
7783
+ reason: "no_source_type",
7784
+ sourceId: null,
7785
+ edgeType: typeof edge?.edge_type === "string" ? edge.edge_type : null,
7786
+ sourceNodeType: null,
7787
+ targetNodeType: null
7788
+ });
7789
+ }
7790
+ }
7475
7791
  return { okf_type: null, edges: [] };
7476
7792
  }
7477
7793
  const edges = validateInlineEdges(canonical, null, fact.edges ?? [], manifest, opts);
@@ -7484,18 +7800,39 @@ var OntologyService = class {
7484
7800
  * writes inside their own transaction (e.g. `IngestionService.ingestDocument`
7485
7801
  * when delegating to `upsertGraphCore`).
7486
7802
  */
7487
- resolveEdges(entityId, sourceId, sourceType, edges, manifest, titleIndex, now) {
7488
- if (!sourceType || edges.length === 0) return [];
7803
+ resolveEdges(entityId, sourceId, sourceType, edges, manifest, titleIndex, now, drops) {
7804
+ if (edges.length === 0) return [];
7805
+ if (!sourceType) {
7806
+ for (const edge of edges) {
7807
+ drops?.push({ reason: "no_source_type", sourceId, edgeType: edge.edge_type, sourceNodeType: null, targetNodeType: null });
7808
+ }
7809
+ return [];
7810
+ }
7489
7811
  const out = [];
7490
7812
  for (const edge of edges) {
7491
7813
  const candidates = resolveEdgeDefinitions(edge.edge_type, manifest).filter((d) => typeSatisfies(d.source_type, sourceType, manifest));
7492
- if (candidates.length === 0) continue;
7814
+ if (candidates.length === 0) {
7815
+ drops?.push({ reason: "type_not_in_manifest", sourceId, edgeType: edge.edge_type, sourceNodeType: sourceType, targetNodeType: null });
7816
+ continue;
7817
+ }
7493
7818
  const targetKey = normalizeTitleKey(edge.target_title);
7494
7819
  const target = titleIndex.get(targetKey);
7495
- if (!target) continue;
7820
+ if (!target) {
7821
+ drops?.push({ reason: "target_not_found", sourceId, edgeType: edge.edge_type, sourceNodeType: sourceType, targetNodeType: null });
7822
+ continue;
7823
+ }
7496
7824
  const targetType = (target.okf_type ?? "").trim().toLowerCase();
7497
7825
  const def = candidates.find((d) => d.target_type.trim().toLowerCase() === targetType) ?? candidates.find((d) => typeSatisfies(d.target_type, targetType, manifest));
7498
- if (!def) continue;
7826
+ if (!def) {
7827
+ drops?.push({
7828
+ reason: "target_type_mismatch",
7829
+ sourceId,
7830
+ edgeType: edge.edge_type,
7831
+ sourceNodeType: sourceType,
7832
+ targetNodeType: target.okf_type ?? null
7833
+ });
7834
+ continue;
7835
+ }
7499
7836
  out.push({
7500
7837
  id: generateId(),
7501
7838
  entity_id: entityId,
@@ -7514,8 +7851,8 @@ var OntologyService = class {
7514
7851
  * want self-contained edge persistence (e.g. MaintenanceService heal /
7515
7852
  * backfill callers that do not have a separate edge-write step).
7516
7853
  */
7517
- async resolveAndPersistEdges(entityId, sourceId, sourceType, edges, manifest, titleIndex, tx, now) {
7518
- const resolved = this.resolveEdges(entityId, sourceId, sourceType, edges, manifest, titleIndex, now);
7854
+ async resolveAndPersistEdges(entityId, sourceId, sourceType, edges, manifest, titleIndex, tx, now, drops) {
7855
+ const resolved = this.resolveEdges(entityId, sourceId, sourceType, edges, manifest, titleIndex, now, drops);
7519
7856
  let persisted = 0;
7520
7857
  for (const edge of resolved) {
7521
7858
  const inserted = await this.edgeRepo.addIgnoreDuplicate(edge, tx);
@@ -8136,11 +8473,15 @@ var WikiMemory = class {
8136
8473
  attemptedSourceRef: sourceRef
8137
8474
  });
8138
8475
  }
8139
- return this.ingestionService.upsertGraphCore(
8476
+ const diagBuffer = new DiagnosticBuffer();
8477
+ const result = await this.ingestionService.upsertGraphCore(
8140
8478
  entityId,
8141
8479
  { sourceRef, sourceHash, nodes: params.nodes, edges: params.edges },
8142
- adapter
8480
+ adapter,
8481
+ { diag: { buffer: diagBuffer, operation: "upsertGraph" } }
8143
8482
  );
8483
+ diagBuffer.flush(this.options);
8484
+ return result;
8144
8485
  }
8145
8486
  /**
8146
8487
  * Returns up to `limit` unprocessed outbox events, oldest first.