@anuma/sdk 6.19.2 → 6.21.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.
@@ -6912,7 +6912,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
6912
6912
  subQueries = decomp.subQueries;
6913
6913
  }
6914
6914
  }
6915
- const { recall: recall2 } = await import("./recall-SCMH2R24.mjs");
6915
+ const { recall: recall2 } = await import("./recall-FQQGE6A2.mjs");
6916
6916
  let recallDegraded = [];
6917
6917
  const result = await recall2(
6918
6918
  query,
@@ -7503,7 +7503,22 @@ async function recall(query, ctx, options = {}) {
7503
7503
  let factLaneRankedOnCosine = false;
7504
7504
  let decryptLastRan;
7505
7505
  let vaultRowsDecrypted;
7506
- const emitDiagnostics = (candidateCount) => {
7506
+ let graphLaneCount = 0;
7507
+ let temporalLaneCount = 0;
7508
+ let graphLaneFailed = false;
7509
+ let temporalLaneFailed = false;
7510
+ let hitLimit = false;
7511
+ let factFloor = -1;
7512
+ let chunkFloor = -1;
7513
+ const emptyReasonFor = (admitted, laneRan) => {
7514
+ if (admitted > 0) return "";
7515
+ if (!query || typeof query !== "string" || query.trim().length === 0) return "empty-query";
7516
+ if (!laneRan) return "no-lanes";
7517
+ const chunkLaneCouldAnswer = types.includes("chunk") && !!ctx.storageCtx;
7518
+ if (vaultSize === 0 && !chunkLaneCouldAnswer) return "vault-empty";
7519
+ return "no-candidates";
7520
+ };
7521
+ const emitDiagnostics = (candidateCount, admitted) => {
7507
7522
  const cb = options.onDiagnostics;
7508
7523
  if (!cb) return;
7509
7524
  const degraded = [];
@@ -7514,6 +7529,11 @@ async function recall(query, ctx, options = {}) {
7514
7529
  degraded.push("decompose-moved");
7515
7530
  }
7516
7531
  if (embeddingsUnavailable) degraded.push("embeddings-unavailable");
7532
+ if (graphLaneFailed) degraded.push("graph-lane-failed");
7533
+ if (temporalLaneFailed) degraded.push("temporal-lane-failed");
7534
+ const laneRan = types.includes("fact") && !!ctx.vaultCtx && !!ctx.vaultCache || types.includes("chunk") && !!ctx.storageCtx;
7535
+ const scores = admitted.map((m) => m.score);
7536
+ const minScoreApplied = factResults.length > 0 ? factFloor : chunkResults.length > 0 ? chunkFloor : factFloor >= 0 ? factFloor : chunkFloor;
7517
7537
  const diagnostics = {
7518
7538
  usedBudget,
7519
7539
  reranked: didRerank,
@@ -7524,6 +7544,14 @@ async function recall(query, ctx, options = {}) {
7524
7544
  ...vaultRowsEmbedded !== void 0 && { vaultRowsEmbedded },
7525
7545
  factCount: factResults.length,
7526
7546
  chunkCount: chunkResults.length,
7547
+ admittedCount: admitted.length,
7548
+ topScore: scores.length > 0 ? Math.max(...scores) : -1,
7549
+ lowestAdmittedScore: scores.length > 0 ? Math.min(...scores) : -1,
7550
+ minScoreApplied,
7551
+ truncated: hitLimit,
7552
+ graphLaneCount,
7553
+ temporalLaneCount,
7554
+ emptyReason: emptyReasonFor(admitted.length, laneRan),
7527
7555
  timings: {
7528
7556
  total: nowMs2() - t0,
7529
7557
  prep: prepMs,
@@ -7541,7 +7569,7 @@ async function recall(query, ctx, options = {}) {
7541
7569
  }
7542
7570
  };
7543
7571
  if (!query || typeof query !== "string" || query.trim().length === 0) {
7544
- emitDiagnostics(0);
7572
+ emitDiagnostics(0, []);
7545
7573
  return { memories: [], usedBudget, reranked: false, candidateCount: 0 };
7546
7574
  }
7547
7575
  const needsChunkEmbedding = types.includes("chunk") && ctx.storageCtx;
@@ -7575,6 +7603,7 @@ async function recall(query, ctx, options = {}) {
7575
7603
  // empty ranking instead (mirrors safeCountVault's fail-soft posture).
7576
7604
  safeLane(
7577
7605
  "graph",
7606
+ () => graphLaneFailed = true,
7578
7607
  () => buildGraphLaneRanking(query, ctx, flags.traverse, {
7579
7608
  ...options.maxHops !== void 0 && { maxHops: options.maxHops },
7580
7609
  ...options.entityFanout !== void 0 && { entityFanout: options.entityFanout },
@@ -7583,12 +7612,19 @@ async function recall(query, ctx, options = {}) {
7583
7612
  ...graphRefiner && { refineNeighbors: graphRefiner }
7584
7613
  })
7585
7614
  ),
7586
- wantsTemporal ? safeLane("temporal", () => buildTemporalLaneRanking(query, ctx.vaultCtx, options.now)) : Promise.resolve([])
7615
+ wantsTemporal ? safeLane(
7616
+ "temporal",
7617
+ () => temporalLaneFailed = true,
7618
+ () => buildTemporalLaneRanking(query, ctx.vaultCtx, options.now)
7619
+ ) : Promise.resolve([])
7587
7620
  ]);
7588
7621
  prepMs = nowMs2() - prepStart;
7622
+ graphLaneCount = entityRanking.length;
7623
+ temporalLaneCount = temporalRanking.length;
7589
7624
  if (types.includes("fact") && ctx.vaultCtx && ctx.vaultCache) {
7590
7625
  const factStart = nowMs2();
7591
7626
  const vaultMinScore = options.minScore ?? DEFAULT_FACT_MIN_SCORE;
7627
+ factFloor = vaultMinScore;
7592
7628
  const {
7593
7629
  results,
7594
7630
  vaultSize: size,
@@ -7668,6 +7704,7 @@ async function recall(query, ctx, options = {}) {
7668
7704
  if (types.includes("chunk") && ctx.storageCtx && queryEmbedding) {
7669
7705
  const chunkStart = nowMs2();
7670
7706
  const chunkMinScore = options.minScore ?? DEFAULT_CHUNK_MIN_SCORE;
7707
+ chunkFloor = chunkMinScore;
7671
7708
  const results = await searchChunksOp(ctx.storageCtx, queryEmbedding, {
7672
7709
  limit: types.includes("fact") ? Math.max(limit * 2, 16) : limit,
7673
7710
  minSimilarity: chunkMinScore,
@@ -7694,9 +7731,11 @@ async function recall(query, ctx, options = {}) {
7694
7731
  memories2.sort((a, b) => b.score - a.score);
7695
7732
  const candidateCount = factResults.length + chunkResults.length;
7696
7733
  fuseMs = nowMs2() - fuseStart;
7697
- emitDiagnostics(candidateCount);
7734
+ const admitted = memories2.slice(0, limit);
7735
+ hitLimit = memories2.length > limit;
7736
+ emitDiagnostics(candidateCount, admitted);
7698
7737
  return {
7699
- memories: memories2.slice(0, limit),
7738
+ memories: admitted,
7700
7739
  usedBudget,
7701
7740
  reranked: didRerank,
7702
7741
  candidateCount,
@@ -7726,12 +7765,16 @@ async function recall(query, ctx, options = {}) {
7726
7765
  const ordered = [...byId.values()].sort((a, b) => b.score - a.score);
7727
7766
  const selectWith = (suppressed2) => {
7728
7767
  const out = [];
7768
+ let cut = false;
7729
7769
  for (const m of ordered) {
7730
- if (out.length >= limit) break;
7731
7770
  if (m.kind === "chunk" && m.messageId && suppressed2.has(m.messageId)) continue;
7771
+ if (out.length >= limit) {
7772
+ cut = true;
7773
+ break;
7774
+ }
7732
7775
  out.push(m);
7733
7776
  }
7734
- return out;
7777
+ return { out, cut };
7735
7778
  };
7736
7779
  const provenanceOf = (selected) => {
7737
7780
  const s = /* @__PURE__ */ new Set();
@@ -7741,15 +7784,18 @@ async function recall(query, ctx, options = {}) {
7741
7784
  return s;
7742
7785
  };
7743
7786
  let suppressed = /* @__PURE__ */ new Set();
7744
- let memories = selectWith(suppressed);
7787
+ let selection = selectWith(suppressed);
7788
+ let memories = selection.out;
7745
7789
  for (let i = 0; i < ordered.length; i++) {
7746
7790
  const next = provenanceOf(memories);
7747
7791
  if (next.size === suppressed.size && [...next].every((id) => suppressed.has(id))) break;
7748
7792
  suppressed = next;
7749
- memories = selectWith(suppressed);
7793
+ selection = selectWith(suppressed);
7794
+ memories = selection.out;
7750
7795
  }
7796
+ hitLimit = selection.cut;
7751
7797
  fuseMs = nowMs2() - fuseStart;
7752
- emitDiagnostics(byId.size);
7798
+ emitDiagnostics(byId.size, memories);
7753
7799
  return {
7754
7800
  memories,
7755
7801
  usedBudget,
@@ -7833,10 +7879,11 @@ async function safeCountVault(ctx) {
7833
7879
  return void 0;
7834
7880
  }
7835
7881
  }
7836
- async function safeLane(label, run) {
7882
+ async function safeLane(label, onFailure, run) {
7837
7883
  try {
7838
7884
  return await run();
7839
7885
  } catch (err) {
7886
+ onFailure();
7840
7887
  getLogger().warn(
7841
7888
  `[memory/recall] ${label} lane failed; continuing without it: ${err instanceof Error ? err.message : String(err)}`
7842
7889
  );
@@ -7678,7 +7678,22 @@ async function recall(query, ctx, options = {}) {
7678
7678
  let factLaneRankedOnCosine = false;
7679
7679
  let decryptLastRan;
7680
7680
  let vaultRowsDecrypted;
7681
- const emitDiagnostics = (candidateCount) => {
7681
+ let graphLaneCount = 0;
7682
+ let temporalLaneCount = 0;
7683
+ let graphLaneFailed = false;
7684
+ let temporalLaneFailed = false;
7685
+ let hitLimit = false;
7686
+ let factFloor = -1;
7687
+ let chunkFloor = -1;
7688
+ const emptyReasonFor = (admitted, laneRan) => {
7689
+ if (admitted > 0) return "";
7690
+ if (!query || typeof query !== "string" || query.trim().length === 0) return "empty-query";
7691
+ if (!laneRan) return "no-lanes";
7692
+ const chunkLaneCouldAnswer = types.includes("chunk") && !!ctx.storageCtx;
7693
+ if (vaultSize === 0 && !chunkLaneCouldAnswer) return "vault-empty";
7694
+ return "no-candidates";
7695
+ };
7696
+ const emitDiagnostics = (candidateCount, admitted) => {
7682
7697
  const cb = options.onDiagnostics;
7683
7698
  if (!cb) return;
7684
7699
  const degraded = [];
@@ -7689,6 +7704,11 @@ async function recall(query, ctx, options = {}) {
7689
7704
  degraded.push("decompose-moved");
7690
7705
  }
7691
7706
  if (embeddingsUnavailable) degraded.push("embeddings-unavailable");
7707
+ if (graphLaneFailed) degraded.push("graph-lane-failed");
7708
+ if (temporalLaneFailed) degraded.push("temporal-lane-failed");
7709
+ const laneRan = types.includes("fact") && !!ctx.vaultCtx && !!ctx.vaultCache || types.includes("chunk") && !!ctx.storageCtx;
7710
+ const scores = admitted.map((m) => m.score);
7711
+ const minScoreApplied = factResults.length > 0 ? factFloor : chunkResults.length > 0 ? chunkFloor : factFloor >= 0 ? factFloor : chunkFloor;
7692
7712
  const diagnostics = {
7693
7713
  usedBudget,
7694
7714
  reranked: didRerank,
@@ -7699,6 +7719,14 @@ async function recall(query, ctx, options = {}) {
7699
7719
  ...vaultRowsEmbedded !== void 0 && { vaultRowsEmbedded },
7700
7720
  factCount: factResults.length,
7701
7721
  chunkCount: chunkResults.length,
7722
+ admittedCount: admitted.length,
7723
+ topScore: scores.length > 0 ? Math.max(...scores) : -1,
7724
+ lowestAdmittedScore: scores.length > 0 ? Math.min(...scores) : -1,
7725
+ minScoreApplied,
7726
+ truncated: hitLimit,
7727
+ graphLaneCount,
7728
+ temporalLaneCount,
7729
+ emptyReason: emptyReasonFor(admitted.length, laneRan),
7702
7730
  timings: {
7703
7731
  total: nowMs() - t0,
7704
7732
  prep: prepMs,
@@ -7716,7 +7744,7 @@ async function recall(query, ctx, options = {}) {
7716
7744
  }
7717
7745
  };
7718
7746
  if (!query || typeof query !== "string" || query.trim().length === 0) {
7719
- emitDiagnostics(0);
7747
+ emitDiagnostics(0, []);
7720
7748
  return { memories: [], usedBudget, reranked: false, candidateCount: 0 };
7721
7749
  }
7722
7750
  const needsChunkEmbedding = types.includes("chunk") && ctx.storageCtx;
@@ -7750,6 +7778,7 @@ async function recall(query, ctx, options = {}) {
7750
7778
  // empty ranking instead (mirrors safeCountVault's fail-soft posture).
7751
7779
  safeLane(
7752
7780
  "graph",
7781
+ () => graphLaneFailed = true,
7753
7782
  () => buildGraphLaneRanking(query, ctx, flags.traverse, {
7754
7783
  ...options.maxHops !== void 0 && { maxHops: options.maxHops },
7755
7784
  ...options.entityFanout !== void 0 && { entityFanout: options.entityFanout },
@@ -7758,12 +7787,19 @@ async function recall(query, ctx, options = {}) {
7758
7787
  ...graphRefiner && { refineNeighbors: graphRefiner }
7759
7788
  })
7760
7789
  ),
7761
- wantsTemporal ? safeLane("temporal", () => buildTemporalLaneRanking(query, ctx.vaultCtx, options.now)) : Promise.resolve([])
7790
+ wantsTemporal ? safeLane(
7791
+ "temporal",
7792
+ () => temporalLaneFailed = true,
7793
+ () => buildTemporalLaneRanking(query, ctx.vaultCtx, options.now)
7794
+ ) : Promise.resolve([])
7762
7795
  ]);
7763
7796
  prepMs = nowMs() - prepStart;
7797
+ graphLaneCount = entityRanking.length;
7798
+ temporalLaneCount = temporalRanking.length;
7764
7799
  if (types.includes("fact") && ctx.vaultCtx && ctx.vaultCache) {
7765
7800
  const factStart = nowMs();
7766
7801
  const vaultMinScore = options.minScore ?? DEFAULT_FACT_MIN_SCORE;
7802
+ factFloor = vaultMinScore;
7767
7803
  const {
7768
7804
  results,
7769
7805
  vaultSize: size,
@@ -7843,6 +7879,7 @@ async function recall(query, ctx, options = {}) {
7843
7879
  if (types.includes("chunk") && ctx.storageCtx && queryEmbedding) {
7844
7880
  const chunkStart = nowMs();
7845
7881
  const chunkMinScore = options.minScore ?? DEFAULT_CHUNK_MIN_SCORE;
7882
+ chunkFloor = chunkMinScore;
7846
7883
  const results = await searchChunksOp(ctx.storageCtx, queryEmbedding, {
7847
7884
  limit: types.includes("fact") ? Math.max(limit * 2, 16) : limit,
7848
7885
  minSimilarity: chunkMinScore,
@@ -7869,9 +7906,11 @@ async function recall(query, ctx, options = {}) {
7869
7906
  memories2.sort((a, b) => b.score - a.score);
7870
7907
  const candidateCount = factResults.length + chunkResults.length;
7871
7908
  fuseMs = nowMs() - fuseStart;
7872
- emitDiagnostics(candidateCount);
7909
+ const admitted = memories2.slice(0, limit);
7910
+ hitLimit = memories2.length > limit;
7911
+ emitDiagnostics(candidateCount, admitted);
7873
7912
  return {
7874
- memories: memories2.slice(0, limit),
7913
+ memories: admitted,
7875
7914
  usedBudget,
7876
7915
  reranked: didRerank,
7877
7916
  candidateCount,
@@ -7901,12 +7940,16 @@ async function recall(query, ctx, options = {}) {
7901
7940
  const ordered = [...byId.values()].sort((a, b) => b.score - a.score);
7902
7941
  const selectWith = (suppressed2) => {
7903
7942
  const out = [];
7943
+ let cut = false;
7904
7944
  for (const m of ordered) {
7905
- if (out.length >= limit) break;
7906
7945
  if (m.kind === "chunk" && m.messageId && suppressed2.has(m.messageId)) continue;
7946
+ if (out.length >= limit) {
7947
+ cut = true;
7948
+ break;
7949
+ }
7907
7950
  out.push(m);
7908
7951
  }
7909
- return out;
7952
+ return { out, cut };
7910
7953
  };
7911
7954
  const provenanceOf = (selected) => {
7912
7955
  const s = /* @__PURE__ */ new Set();
@@ -7916,15 +7959,18 @@ async function recall(query, ctx, options = {}) {
7916
7959
  return s;
7917
7960
  };
7918
7961
  let suppressed = /* @__PURE__ */ new Set();
7919
- let memories = selectWith(suppressed);
7962
+ let selection = selectWith(suppressed);
7963
+ let memories = selection.out;
7920
7964
  for (let i = 0; i < ordered.length; i++) {
7921
7965
  const next = provenanceOf(memories);
7922
7966
  if (next.size === suppressed.size && [...next].every((id) => suppressed.has(id))) break;
7923
7967
  suppressed = next;
7924
- memories = selectWith(suppressed);
7968
+ selection = selectWith(suppressed);
7969
+ memories = selection.out;
7925
7970
  }
7971
+ hitLimit = selection.cut;
7926
7972
  fuseMs = nowMs() - fuseStart;
7927
- emitDiagnostics(byId.size);
7973
+ emitDiagnostics(byId.size, memories);
7928
7974
  return {
7929
7975
  memories,
7930
7976
  usedBudget,
@@ -8008,10 +8054,11 @@ async function safeCountVault(ctx) {
8008
8054
  return void 0;
8009
8055
  }
8010
8056
  }
8011
- async function safeLane(label, run) {
8057
+ async function safeLane(label, onFailure, run) {
8012
8058
  try {
8013
8059
  return await run();
8014
8060
  } catch (err) {
8061
+ onFailure();
8015
8062
  getLogger().warn(
8016
8063
  `[memory/recall] ${label} lane failed; continuing without it: ${err instanceof Error ? err.message : String(err)}`
8017
8064
  );
@@ -16385,6 +16432,7 @@ async function retain(content, ctx, options = {}) {
16385
16432
  const threshold = options.autoMergeThreshold ?? DEFAULT_AUTO_MERGE_THRESHOLD;
16386
16433
  const resolvedScope = options.scope ?? DEFAULT_SCOPE;
16387
16434
  let supersedeTargetIds = [];
16435
+ let consolidationDecidedCreate = false;
16388
16436
  let supersedeContent;
16389
16437
  let prepared;
16390
16438
  if (enableAutoMerge) {
@@ -16412,8 +16460,12 @@ async function retain(content, ctx, options = {}) {
16412
16460
  const outcome = await tryConsolidate(trimmed, ctx, options, prepared);
16413
16461
  if (outcome) {
16414
16462
  if ("done" in outcome) return outcome.done;
16415
- supersedeTargetIds = outcome.supersede;
16416
- supersedeContent = outcome.content;
16463
+ if ("create" in outcome) {
16464
+ consolidationDecidedCreate = true;
16465
+ } else {
16466
+ supersedeTargetIds = outcome.supersede;
16467
+ supersedeContent = outcome.content;
16468
+ }
16417
16469
  }
16418
16470
  }
16419
16471
  if (supersedeTargetIds.length === 0) {
@@ -16463,7 +16515,14 @@ async function retain(content, ctx, options = {}) {
16463
16515
  action: "merge",
16464
16516
  memoryId: targetId,
16465
16517
  targetId,
16466
- proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1
16518
+ proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1,
16519
+ similarity: matches[0].similarity,
16520
+ // Stage 1 may have run and explicitly said `create`, and Stage 2
16521
+ // then found a strict-cosine match anyway. Reporting the decision
16522
+ // here is what makes that DISAGREEMENT visible — without it the
16523
+ // model's create silently vanished from the distribution whenever
16524
+ // the cosine stage won.
16525
+ ...consolidationDecidedCreate && { consolidation: "create" }
16467
16526
  };
16468
16527
  }
16469
16528
  await assertMergeTargetGoneOrThrow(ctx, targetId);
@@ -16476,12 +16535,18 @@ async function retain(content, ctx, options = {}) {
16476
16535
  const embedding = reusableQueryEmbedding ?? await generateEmbedding(contentToWrite, ctx.embeddingOptions);
16477
16536
  const embeddingModel = ctx.embeddingOptions.model ?? DEFAULT_API_EMBEDDING_MODEL;
16478
16537
  if (options.respectTombstones) {
16479
- const tombstoneId = await findTombstoneMatch(embedding, embeddingModel, resolvedScope, ctx, {
16538
+ const tombstone = await findTombstoneMatch(embedding, embeddingModel, resolvedScope, ctx, {
16480
16539
  threshold,
16481
16540
  folderId: options.folderId
16482
16541
  });
16483
- if (tombstoneId) {
16484
- return { action: "suppressed", memoryId: tombstoneId, tombstoneId, proofCount: 0 };
16542
+ if (tombstone) {
16543
+ return {
16544
+ action: "suppressed",
16545
+ memoryId: tombstone.id,
16546
+ tombstoneId: tombstone.id,
16547
+ proofCount: 0,
16548
+ similarity: tombstone.similarity
16549
+ };
16485
16550
  }
16486
16551
  }
16487
16552
  const createOpts = {
@@ -16548,7 +16613,8 @@ async function retain(content, ctx, options = {}) {
16548
16613
  action: "supersede",
16549
16614
  memoryId: created2.uniqueId,
16550
16615
  targetId: primaryTargetId,
16551
- proofCount: 1
16616
+ proofCount: 1,
16617
+ consolidation: "supersede"
16552
16618
  };
16553
16619
  }
16554
16620
  notifyConsolidationFallback(
@@ -16562,7 +16628,8 @@ async function retain(content, ctx, options = {}) {
16562
16628
  return {
16563
16629
  action: "create",
16564
16630
  memoryId: created.uniqueId,
16565
- proofCount: 1
16631
+ proofCount: 1,
16632
+ ...consolidationDecidedCreate && { consolidation: "create" }
16566
16633
  };
16567
16634
  }
16568
16635
  function unionStrings(a, b) {
@@ -16592,7 +16659,7 @@ async function findTombstoneMatch(embedding, embeddingModel, scope, ctx, opts) {
16592
16659
  bestId = row.uniqueId;
16593
16660
  }
16594
16661
  }
16595
- return bestId;
16662
+ return bestId === null ? null : { id: bestId, similarity: bestSim };
16596
16663
  }
16597
16664
  function pickEventTimeUpdate(existing, incoming) {
16598
16665
  if (incoming === void 0 || incoming === null) return void 0;
@@ -16634,7 +16701,7 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
16634
16701
  }));
16635
16702
  const { consolidateMemory: doConsolidate } = await Promise.resolve().then(() => (init_consolidate(), consolidate_exports));
16636
16703
  const decision = await doConsolidate(trimmed, candidates, consolidateOptions);
16637
- if (decision.action === "create") return null;
16704
+ if (decision.action === "create") return decision.fallbackReason ? null : { create: true };
16638
16705
  if (decision.action === "supersede" && decision.content) {
16639
16706
  const requestedIds = decision.targetIds?.length ? decision.targetIds : decision.targetId ? [decision.targetId] : [];
16640
16707
  if (requestedIds.length === 0) return null;
@@ -16693,7 +16760,8 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
16693
16760
  action: "merge",
16694
16761
  memoryId: decision.targetId,
16695
16762
  targetId: decision.targetId,
16696
- proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1
16763
+ proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1,
16764
+ consolidation: "noop"
16697
16765
  }
16698
16766
  };
16699
16767
  }
@@ -16746,7 +16814,8 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
16746
16814
  action: "update",
16747
16815
  memoryId: decision.targetId,
16748
16816
  targetId: decision.targetId,
16749
- proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1
16817
+ proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1,
16818
+ consolidation: "update"
16750
16819
  }
16751
16820
  };
16752
16821
  }
@@ -16888,6 +16957,11 @@ Extract durable user facts.`,
16888
16957
  fallbackSourceId,
16889
16958
  options.userIdentity ?? []
16890
16959
  );
16960
+ const rawCandidates = parsed.candidates;
16961
+ options.onCandidatesParsed?.({
16962
+ rawCount: Array.isArray(rawCandidates) ? rawCandidates.length : 0,
16963
+ validCount: candidates.length
16964
+ });
16891
16965
  if (!redactor) return candidates;
16892
16966
  const restored = restoreCandidates(candidates, redactor, options.userIdentity ?? []);
16893
16967
  if (candidates.length > 0 && restored.length === 0) {
@@ -16924,8 +16998,12 @@ async function extractAndRetain(messages, retainCtx, options) {
16924
16998
  let exhaustedEmpty = false;
16925
16999
  let exhaustedFailure;
16926
17000
  let droppedAfterRedaction = false;
17001
+ let rawCandidateCount = 0;
17002
+ let validCandidateCount = 0;
16927
17003
  const callerOnExhaustedEmpty = options.extract.onExhaustedEmpty;
16928
17004
  const callerOnCandidatesDropped = options.extract.onCandidatesDropped;
17005
+ const callerOnCandidatesParsed = options.extract.onCandidatesParsed;
17006
+ const tExtract = Date.now();
16929
17007
  const candidates = await extractFacts(messages, {
16930
17008
  ...options.extract,
16931
17009
  onExhaustedEmpty: (failure) => {
@@ -16936,8 +17014,14 @@ async function extractAndRetain(messages, retainCtx, options) {
16936
17014
  onCandidatesDropped: () => {
16937
17015
  droppedAfterRedaction = true;
16938
17016
  callerOnCandidatesDropped?.();
17017
+ },
17018
+ onCandidatesParsed: (stats) => {
17019
+ rawCandidateCount = stats.rawCount;
17020
+ validCandidateCount = stats.validCount;
17021
+ callerOnCandidatesParsed?.(stats);
16939
17022
  }
16940
17023
  });
17024
+ const extractMs = Date.now() - tExtract;
16941
17025
  const filtered = candidates.filter((c) => c.confidence >= minConfidence);
16942
17026
  const log = getLogger();
16943
17027
  const screened = screenCandidatesForInjection(filtered);
@@ -16985,6 +17069,7 @@ async function extractAndRetain(messages, retainCtx, options) {
16985
17069
  const results = [];
16986
17070
  const quarantinedInfo = [];
16987
17071
  let failedWrites = 0;
17072
+ const tRetain = Date.now();
16988
17073
  for (const { candidate, isQuarantined, reason, signature } of toRetain) {
16989
17074
  try {
16990
17075
  const result = await retain(candidate.content, retainCtx, {
@@ -17051,6 +17136,21 @@ async function extractAndRetain(messages, retainCtx, options) {
17051
17136
  failedCount: failedWrites,
17052
17137
  outcome,
17053
17138
  quarantined: quarantinedInfo,
17139
+ funnel: {
17140
+ rawCandidateCount,
17141
+ validCandidateCount,
17142
+ afterRedactionCount: candidates.length,
17143
+ aboveConfidenceCount: filtered.length,
17144
+ // The PERSISTED quarantines (`quarantinedInfo`), not the screened list: a
17145
+ // screened candidate whose retain() throws is counted in `failedCount`,
17146
+ // and counting it here too would double it and break the identity below.
17147
+ // This also keeps the count equal to `quarantined.length` on the result.
17148
+ quarantinedCount: quarantinedInfo.length,
17149
+ retainedCount: results.length,
17150
+ failedCount: failedWrites
17151
+ },
17152
+ timings: { extractMs, retainMs: Date.now() - tRetain },
17153
+ model: options.extract.model ?? DEFAULT_EXTRACTION_MODEL,
17054
17154
  // Only meaningful alongside `outcome: "empty-after-retry"`. Returned as well
17055
17155
  // as pushed through the callback so a consumer that only inspects the result
17056
17156
  // (rather than wiring a hook) can still report WHY.
@@ -17306,27 +17406,23 @@ function createAutoExtractor(options) {
17306
17406
  const t0 = Date.now();
17307
17407
  void (async () => {
17308
17408
  try {
17309
- const { candidates, results, failedCount, outcome, failure } = await extractAndRetain(
17310
- window2,
17311
- retainCtx,
17312
- {
17313
- extract,
17314
- ...options.minConfidence !== void 0 && { minConfidence: options.minConfidence },
17315
- ...options.entityCtx !== void 0 && { entityCtx: options.entityCtx },
17316
- ...options.scope !== void 0 && { scope: options.scope },
17317
- ...options.folderId !== void 0 && { folderId: options.folderId },
17318
- ...consolidateOptions !== void 0 && { consolidateOptions },
17319
- ...injectionClassifierOptions !== void 0 && {
17320
- injectionClassifier: injectionClassifierOptions
17321
- },
17322
- ...options.onCandidateFailed && {
17323
- onCandidateFailed: (candidate, error) => options.onCandidateFailed?.({ candidate, error, conversationId })
17324
- },
17325
- ...options.onMemoryQuarantined && {
17326
- onQuarantined: (info) => options.onMemoryQuarantined?.({ ...info, conversationId })
17327
- }
17409
+ const { candidates, results, failedCount, outcome, failure, funnel, timings, model } = await extractAndRetain(window2, retainCtx, {
17410
+ extract,
17411
+ ...options.minConfidence !== void 0 && { minConfidence: options.minConfidence },
17412
+ ...options.entityCtx !== void 0 && { entityCtx: options.entityCtx },
17413
+ ...options.scope !== void 0 && { scope: options.scope },
17414
+ ...options.folderId !== void 0 && { folderId: options.folderId },
17415
+ ...consolidateOptions !== void 0 && { consolidateOptions },
17416
+ ...injectionClassifierOptions !== void 0 && {
17417
+ injectionClassifier: injectionClassifierOptions
17418
+ },
17419
+ ...options.onCandidateFailed && {
17420
+ onCandidateFailed: (candidate, error) => options.onCandidateFailed?.({ candidate, error, conversationId })
17421
+ },
17422
+ ...options.onMemoryQuarantined && {
17423
+ onQuarantined: (info) => options.onMemoryQuarantined?.({ ...info, conversationId })
17328
17424
  }
17329
- );
17425
+ });
17330
17426
  if (outcome !== "empty-after-retry") {
17331
17427
  const advancedTo = window2[window2.length - 1].id;
17332
17428
  stateFor(conversationId).watermark = advancedTo;
@@ -17347,6 +17443,9 @@ function createAutoExtractor(options) {
17347
17443
  durationMs: Date.now() - t0,
17348
17444
  conversationId,
17349
17445
  outcome,
17446
+ funnel,
17447
+ timings,
17448
+ model,
17350
17449
  // Only set alongside `outcome: "empty-after-retry"`. Spread so the key
17351
17450
  // is absent rather than explicitly undefined on a healthy turn —
17352
17451
  // analytics backends store an explicit undefined as a real value.