@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.
@@ -141,7 +141,7 @@ import {
141
141
  updateVaultMemoryOp,
142
142
  upsertMessageOp,
143
143
  vaultMemoryToStored
144
- } from "./chunk-X5EZQFGR.mjs";
144
+ } from "./chunk-4DSDATYG.mjs";
145
145
  import {
146
146
  createStreamingDeAnonymizer,
147
147
  notifyConsolidationFallback,
@@ -5496,6 +5496,7 @@ async function retain(content, ctx, options = {}) {
5496
5496
  const threshold = options.autoMergeThreshold ?? DEFAULT_AUTO_MERGE_THRESHOLD;
5497
5497
  const resolvedScope = options.scope ?? DEFAULT_SCOPE;
5498
5498
  let supersedeTargetIds = [];
5499
+ let consolidationDecidedCreate = false;
5499
5500
  let supersedeContent;
5500
5501
  let prepared;
5501
5502
  if (enableAutoMerge) {
@@ -5523,8 +5524,12 @@ async function retain(content, ctx, options = {}) {
5523
5524
  const outcome = await tryConsolidate(trimmed, ctx, options, prepared);
5524
5525
  if (outcome) {
5525
5526
  if ("done" in outcome) return outcome.done;
5526
- supersedeTargetIds = outcome.supersede;
5527
- supersedeContent = outcome.content;
5527
+ if ("create" in outcome) {
5528
+ consolidationDecidedCreate = true;
5529
+ } else {
5530
+ supersedeTargetIds = outcome.supersede;
5531
+ supersedeContent = outcome.content;
5532
+ }
5528
5533
  }
5529
5534
  }
5530
5535
  if (supersedeTargetIds.length === 0) {
@@ -5574,7 +5579,14 @@ async function retain(content, ctx, options = {}) {
5574
5579
  action: "merge",
5575
5580
  memoryId: targetId,
5576
5581
  targetId,
5577
- proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1
5582
+ proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1,
5583
+ similarity: matches[0].similarity,
5584
+ // Stage 1 may have run and explicitly said `create`, and Stage 2
5585
+ // then found a strict-cosine match anyway. Reporting the decision
5586
+ // here is what makes that DISAGREEMENT visible — without it the
5587
+ // model's create silently vanished from the distribution whenever
5588
+ // the cosine stage won.
5589
+ ...consolidationDecidedCreate && { consolidation: "create" }
5578
5590
  };
5579
5591
  }
5580
5592
  await assertMergeTargetGoneOrThrow(ctx, targetId);
@@ -5587,12 +5599,18 @@ async function retain(content, ctx, options = {}) {
5587
5599
  const embedding = reusableQueryEmbedding ?? await generateEmbedding(contentToWrite, ctx.embeddingOptions);
5588
5600
  const embeddingModel = ctx.embeddingOptions.model ?? DEFAULT_API_EMBEDDING_MODEL;
5589
5601
  if (options.respectTombstones) {
5590
- const tombstoneId = await findTombstoneMatch(embedding, embeddingModel, resolvedScope, ctx, {
5602
+ const tombstone = await findTombstoneMatch(embedding, embeddingModel, resolvedScope, ctx, {
5591
5603
  threshold,
5592
5604
  folderId: options.folderId
5593
5605
  });
5594
- if (tombstoneId) {
5595
- return { action: "suppressed", memoryId: tombstoneId, tombstoneId, proofCount: 0 };
5606
+ if (tombstone) {
5607
+ return {
5608
+ action: "suppressed",
5609
+ memoryId: tombstone.id,
5610
+ tombstoneId: tombstone.id,
5611
+ proofCount: 0,
5612
+ similarity: tombstone.similarity
5613
+ };
5596
5614
  }
5597
5615
  }
5598
5616
  const createOpts = {
@@ -5659,7 +5677,8 @@ async function retain(content, ctx, options = {}) {
5659
5677
  action: "supersede",
5660
5678
  memoryId: created2.uniqueId,
5661
5679
  targetId: primaryTargetId,
5662
- proofCount: 1
5680
+ proofCount: 1,
5681
+ consolidation: "supersede"
5663
5682
  };
5664
5683
  }
5665
5684
  notifyConsolidationFallback(
@@ -5673,7 +5692,8 @@ async function retain(content, ctx, options = {}) {
5673
5692
  return {
5674
5693
  action: "create",
5675
5694
  memoryId: created.uniqueId,
5676
- proofCount: 1
5695
+ proofCount: 1,
5696
+ ...consolidationDecidedCreate && { consolidation: "create" }
5677
5697
  };
5678
5698
  }
5679
5699
  function unionStrings(a, b) {
@@ -5703,7 +5723,7 @@ async function findTombstoneMatch(embedding, embeddingModel, scope, ctx, opts) {
5703
5723
  bestId = row.uniqueId;
5704
5724
  }
5705
5725
  }
5706
- return bestId;
5726
+ return bestId === null ? null : { id: bestId, similarity: bestSim };
5707
5727
  }
5708
5728
  function pickEventTimeUpdate(existing, incoming) {
5709
5729
  if (incoming === void 0 || incoming === null) return void 0;
@@ -5745,7 +5765,7 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
5745
5765
  }));
5746
5766
  const { consolidateMemory: doConsolidate } = await import("./consolidate-54P6QZKU.mjs");
5747
5767
  const decision = await doConsolidate(trimmed, candidates, consolidateOptions);
5748
- if (decision.action === "create") return null;
5768
+ if (decision.action === "create") return decision.fallbackReason ? null : { create: true };
5749
5769
  if (decision.action === "supersede" && decision.content) {
5750
5770
  const requestedIds = decision.targetIds?.length ? decision.targetIds : decision.targetId ? [decision.targetId] : [];
5751
5771
  if (requestedIds.length === 0) return null;
@@ -5804,7 +5824,8 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
5804
5824
  action: "merge",
5805
5825
  memoryId: decision.targetId,
5806
5826
  targetId: decision.targetId,
5807
- proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1
5827
+ proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1,
5828
+ consolidation: "noop"
5808
5829
  }
5809
5830
  };
5810
5831
  }
@@ -5857,7 +5878,8 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
5857
5878
  action: "update",
5858
5879
  memoryId: decision.targetId,
5859
5880
  targetId: decision.targetId,
5860
- proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1
5881
+ proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1,
5882
+ consolidation: "update"
5861
5883
  }
5862
5884
  };
5863
5885
  }
@@ -5999,6 +6021,11 @@ Extract durable user facts.`,
5999
6021
  fallbackSourceId,
6000
6022
  options.userIdentity ?? []
6001
6023
  );
6024
+ const rawCandidates = parsed.candidates;
6025
+ options.onCandidatesParsed?.({
6026
+ rawCount: Array.isArray(rawCandidates) ? rawCandidates.length : 0,
6027
+ validCount: candidates.length
6028
+ });
6002
6029
  if (!redactor) return candidates;
6003
6030
  const restored = restoreCandidates(candidates, redactor, options.userIdentity ?? []);
6004
6031
  if (candidates.length > 0 && restored.length === 0) {
@@ -6035,8 +6062,12 @@ async function extractAndRetain(messages, retainCtx, options) {
6035
6062
  let exhaustedEmpty = false;
6036
6063
  let exhaustedFailure;
6037
6064
  let droppedAfterRedaction = false;
6065
+ let rawCandidateCount = 0;
6066
+ let validCandidateCount = 0;
6038
6067
  const callerOnExhaustedEmpty = options.extract.onExhaustedEmpty;
6039
6068
  const callerOnCandidatesDropped = options.extract.onCandidatesDropped;
6069
+ const callerOnCandidatesParsed = options.extract.onCandidatesParsed;
6070
+ const tExtract = Date.now();
6040
6071
  const candidates = await extractFacts(messages, {
6041
6072
  ...options.extract,
6042
6073
  onExhaustedEmpty: (failure) => {
@@ -6047,8 +6078,14 @@ async function extractAndRetain(messages, retainCtx, options) {
6047
6078
  onCandidatesDropped: () => {
6048
6079
  droppedAfterRedaction = true;
6049
6080
  callerOnCandidatesDropped?.();
6081
+ },
6082
+ onCandidatesParsed: (stats) => {
6083
+ rawCandidateCount = stats.rawCount;
6084
+ validCandidateCount = stats.validCount;
6085
+ callerOnCandidatesParsed?.(stats);
6050
6086
  }
6051
6087
  });
6088
+ const extractMs = Date.now() - tExtract;
6052
6089
  const filtered = candidates.filter((c) => c.confidence >= minConfidence);
6053
6090
  const log = getLogger();
6054
6091
  const screened = screenCandidatesForInjection(filtered);
@@ -6096,6 +6133,7 @@ async function extractAndRetain(messages, retainCtx, options) {
6096
6133
  const results = [];
6097
6134
  const quarantinedInfo = [];
6098
6135
  let failedWrites = 0;
6136
+ const tRetain = Date.now();
6099
6137
  for (const { candidate, isQuarantined, reason, signature } of toRetain) {
6100
6138
  try {
6101
6139
  const result = await retain(candidate.content, retainCtx, {
@@ -6162,6 +6200,21 @@ async function extractAndRetain(messages, retainCtx, options) {
6162
6200
  failedCount: failedWrites,
6163
6201
  outcome,
6164
6202
  quarantined: quarantinedInfo,
6203
+ funnel: {
6204
+ rawCandidateCount,
6205
+ validCandidateCount,
6206
+ afterRedactionCount: candidates.length,
6207
+ aboveConfidenceCount: filtered.length,
6208
+ // The PERSISTED quarantines (`quarantinedInfo`), not the screened list: a
6209
+ // screened candidate whose retain() throws is counted in `failedCount`,
6210
+ // and counting it here too would double it and break the identity below.
6211
+ // This also keeps the count equal to `quarantined.length` on the result.
6212
+ quarantinedCount: quarantinedInfo.length,
6213
+ retainedCount: results.length,
6214
+ failedCount: failedWrites
6215
+ },
6216
+ timings: { extractMs, retainMs: Date.now() - tRetain },
6217
+ model: options.extract.model ?? DEFAULT_EXTRACTION_MODEL,
6165
6218
  // Only meaningful alongside `outcome: "empty-after-retry"`. Returned as well
6166
6219
  // as pushed through the callback so a consumer that only inspects the result
6167
6220
  // (rather than wiring a hook) can still report WHY.
@@ -6415,27 +6468,23 @@ function createAutoExtractor(options) {
6415
6468
  const t0 = Date.now();
6416
6469
  void (async () => {
6417
6470
  try {
6418
- const { candidates, results, failedCount, outcome, failure } = await extractAndRetain(
6419
- window2,
6420
- retainCtx,
6421
- {
6422
- extract,
6423
- ...options.minConfidence !== void 0 && { minConfidence: options.minConfidence },
6424
- ...options.entityCtx !== void 0 && { entityCtx: options.entityCtx },
6425
- ...options.scope !== void 0 && { scope: options.scope },
6426
- ...options.folderId !== void 0 && { folderId: options.folderId },
6427
- ...consolidateOptions !== void 0 && { consolidateOptions },
6428
- ...injectionClassifierOptions !== void 0 && {
6429
- injectionClassifier: injectionClassifierOptions
6430
- },
6431
- ...options.onCandidateFailed && {
6432
- onCandidateFailed: (candidate, error) => options.onCandidateFailed?.({ candidate, error, conversationId })
6433
- },
6434
- ...options.onMemoryQuarantined && {
6435
- onQuarantined: (info) => options.onMemoryQuarantined?.({ ...info, conversationId })
6436
- }
6471
+ const { candidates, results, failedCount, outcome, failure, funnel, timings, model } = await extractAndRetain(window2, retainCtx, {
6472
+ extract,
6473
+ ...options.minConfidence !== void 0 && { minConfidence: options.minConfidence },
6474
+ ...options.entityCtx !== void 0 && { entityCtx: options.entityCtx },
6475
+ ...options.scope !== void 0 && { scope: options.scope },
6476
+ ...options.folderId !== void 0 && { folderId: options.folderId },
6477
+ ...consolidateOptions !== void 0 && { consolidateOptions },
6478
+ ...injectionClassifierOptions !== void 0 && {
6479
+ injectionClassifier: injectionClassifierOptions
6480
+ },
6481
+ ...options.onCandidateFailed && {
6482
+ onCandidateFailed: (candidate, error) => options.onCandidateFailed?.({ candidate, error, conversationId })
6483
+ },
6484
+ ...options.onMemoryQuarantined && {
6485
+ onQuarantined: (info) => options.onMemoryQuarantined?.({ ...info, conversationId })
6437
6486
  }
6438
- );
6487
+ });
6439
6488
  if (outcome !== "empty-after-retry") {
6440
6489
  const advancedTo = window2[window2.length - 1].id;
6441
6490
  stateFor(conversationId).watermark = advancedTo;
@@ -6456,6 +6505,9 @@ function createAutoExtractor(options) {
6456
6505
  durationMs: Date.now() - t0,
6457
6506
  conversationId,
6458
6507
  outcome,
6508
+ funnel,
6509
+ timings,
6510
+ model,
6459
6511
  // Only set alongside `outcome: "empty-after-retry"`. Spread so the key
6460
6512
  // is absent rather than explicitly undefined on a healthy turn —
6461
6513
  // analytics backends store an explicit undefined as a real value.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  recall
3
- } from "./chunk-X5EZQFGR.mjs";
3
+ } from "./chunk-4DSDATYG.mjs";
4
4
  import "./chunk-YXL5VKCO.mjs";
5
5
  import "./chunk-3RPGXXPZ.mjs";
6
6
  export {
@@ -213,6 +213,23 @@ function createRecallDiagnosticsHandler(sink) {
213
213
  usedBudget: diagnostics.usedBudget,
214
214
  reranked: diagnostics.reranked,
215
215
  candidateCount: diagnostics.candidateCount,
216
+ // What the caller actually GOT. `candidateCount` is what was considered,
217
+ // and the two are routinely far apart — the fact lane pulls `limit * 2`
218
+ // when fusing — so a consumer reading candidateCount as "memories this
219
+ // turn received" overcounts.
220
+ admittedCount: diagnostics.admittedCount,
221
+ // Both ends of the admitted range, and the lane counts, ride the EVENT as
222
+ // well as the metrics: a track-only sink is a supported configuration
223
+ // (the module's own PostHog example is exactly that shape), and it could
224
+ // otherwise see neither the bottom of the score range nor which side lane
225
+ // contributed.
226
+ topScore: diagnostics.topScore,
227
+ lowestAdmittedScore: diagnostics.lowestAdmittedScore,
228
+ minScoreApplied: diagnostics.minScoreApplied,
229
+ truncated: diagnostics.truncated,
230
+ emptyReason: diagnostics.emptyReason,
231
+ graphLaneCount: diagnostics.graphLaneCount,
232
+ temporalLaneCount: diagnostics.temporalLaneCount,
216
233
  factCount: diagnostics.factCount,
217
234
  chunkCount: diagnostics.chunkCount,
218
235
  totalMs: diagnostics.timings.total,
@@ -226,8 +243,11 @@ function createRecallDiagnosticsHandler(sink) {
226
243
  metric("recall.duration", ms, { lane });
227
244
  }
228
245
  metric("recall.candidates", diagnostics.candidateCount, {});
246
+ metric("recall.admitted", diagnostics.admittedCount, {});
229
247
  metric("recall.facts", diagnostics.factCount, {});
230
248
  metric("recall.chunks", diagnostics.chunkCount, {});
249
+ metric("recall.lane.graph", diagnostics.graphLaneCount, {});
250
+ metric("recall.lane.temporal", diagnostics.temporalLaneCount, {});
231
251
  if (diagnostics.vaultSize !== void 0) {
232
252
  metric("recall.vault.size", diagnostics.vaultSize, {});
233
253
  }
@@ -422,7 +422,25 @@ type RecallDegradation =
422
422
  * failing back to the single-query path, or a row batch failing while other
423
423
  * rows keep usable vectors) — those are logged, not reported, so this stays a
424
424
  * reliable outage signal rather than a general embedding-error counter. */
425
- | "embeddings-unavailable";
425
+ | "embeddings-unavailable"
426
+ /** The W5 graph (entity) side lane threw and was dropped. Auxiliary, so recall
427
+ * still returned — but its RRF signal is missing from the ranking, and until
428
+ * this existed the drop was a log line and nothing else. */
429
+ | "graph-lane-failed"
430
+ /** The W6 temporal side lane threw and was dropped. Same posture as the graph
431
+ * lane: recall still returned, ranked without the temporal signal. */
432
+ | "temporal-lane-failed";
433
+ /**
434
+ * Why a recall returned nothing. `""` when it returned something, so the field
435
+ * is always present and groupable rather than being absent on the healthy path.
436
+ *
437
+ * The four are different problems: an empty query is a caller bug, no-lanes is a
438
+ * context wiring bug (the requested kinds have no store), vault-empty is a new
439
+ * user, and no-candidates is the only one that is about retrieval quality. They
440
+ * were previously indistinguishable from outside — every one reported
441
+ * `candidateCount: 0`.
442
+ */
443
+ type RecallEmptyReason = "" | "empty-query" | "no-lanes" | "vault-empty" | "no-candidates";
426
444
  /**
427
445
  * Per-call recall observability payload (see {@link RecallOptions.onDiagnostics}).
428
446
  * All timings are wall-clock milliseconds. Lane counts are post-dedupe,
@@ -473,6 +491,50 @@ interface RecallDiagnostics {
473
491
  factCount: number;
474
492
  /** Chunks the chunk lane returned (post-dedupe, pre-fusion). */
475
493
  chunkCount: number;
494
+ /**
495
+ * Memories actually RETURNED — `memories.length` after fusion, cross-lane
496
+ * dedup and the `limit` slice.
497
+ *
498
+ * `candidateCount` is what was considered; this is what the caller got, and
499
+ * the two are routinely far apart (the fact lane pulls `limit * 2` when fusing).
500
+ * Every consumer that wanted "how many memories did this turn actually get"
501
+ * was reading `candidateCount` and overcounting.
502
+ */
503
+ admittedCount: number;
504
+ /** Highest score among the returned memories; -1 when none were returned. */
505
+ topScore: number;
506
+ /** Lowest score among the returned memories; -1 when none were returned. */
507
+ lowestAdmittedScore: number;
508
+ /**
509
+ * The similarity floor the lane that produced these scores actually applied:
510
+ * the fact lane's when it RETURNED results, otherwise the chunk lane's, and
511
+ * **-1 when neither ran** (empty query, unwired context). Gated on results
512
+ * rather than on the lane running, because a fact lane that ran and came
513
+ * back empty filtered none of the scores in the payload — reporting its
514
+ * 0.1 default against chunks that cleared 0.5 corrupted the telemetry.
515
+ * When NOTHING was admitted, the floor a lane did apply is still reported:
516
+ * "searched at this floor, found nothing" is the useful reading.
517
+ *
518
+ * Per-lane rather than one constant because the two defaults differ (0.1 fact
519
+ * / 0.5 chunk), so a single seeded value reported a floor that a chunk-only
520
+ * recall never applied. Reported next to the scores because the scores alone
521
+ * cannot say what they cleared.
522
+ */
523
+ minScoreApplied: number;
524
+ /**
525
+ * Whether the `limit` cut an ELIGIBLE result — recorded at the cut, not
526
+ * derived from `candidateCount > limit`. In the fused path `candidateCount`
527
+ * counts before provenance suppression, so a recall whose suppressed chunks
528
+ * brought it under the limit would otherwise report a truncation that never
529
+ * happened.
530
+ */
531
+ truncated: boolean;
532
+ /** Memory ids the W5 graph (entity) side lane contributed to the fusion. */
533
+ graphLaneCount: number;
534
+ /** Memory ids the W6 temporal side lane contributed to the fusion. */
535
+ temporalLaneCount: number;
536
+ /** Why nothing came back — see {@link RecallEmptyReason}. `""` when something did. */
537
+ emptyReason: RecallEmptyReason;
476
538
  /** Wall-clock phase timings (ms). */
477
539
  timings: {
478
540
  /** Whole `recall()` call. */
@@ -422,7 +422,25 @@ type RecallDegradation =
422
422
  * failing back to the single-query path, or a row batch failing while other
423
423
  * rows keep usable vectors) — those are logged, not reported, so this stays a
424
424
  * reliable outage signal rather than a general embedding-error counter. */
425
- | "embeddings-unavailable";
425
+ | "embeddings-unavailable"
426
+ /** The W5 graph (entity) side lane threw and was dropped. Auxiliary, so recall
427
+ * still returned — but its RRF signal is missing from the ranking, and until
428
+ * this existed the drop was a log line and nothing else. */
429
+ | "graph-lane-failed"
430
+ /** The W6 temporal side lane threw and was dropped. Same posture as the graph
431
+ * lane: recall still returned, ranked without the temporal signal. */
432
+ | "temporal-lane-failed";
433
+ /**
434
+ * Why a recall returned nothing. `""` when it returned something, so the field
435
+ * is always present and groupable rather than being absent on the healthy path.
436
+ *
437
+ * The four are different problems: an empty query is a caller bug, no-lanes is a
438
+ * context wiring bug (the requested kinds have no store), vault-empty is a new
439
+ * user, and no-candidates is the only one that is about retrieval quality. They
440
+ * were previously indistinguishable from outside — every one reported
441
+ * `candidateCount: 0`.
442
+ */
443
+ type RecallEmptyReason = "" | "empty-query" | "no-lanes" | "vault-empty" | "no-candidates";
426
444
  /**
427
445
  * Per-call recall observability payload (see {@link RecallOptions.onDiagnostics}).
428
446
  * All timings are wall-clock milliseconds. Lane counts are post-dedupe,
@@ -473,6 +491,50 @@ interface RecallDiagnostics {
473
491
  factCount: number;
474
492
  /** Chunks the chunk lane returned (post-dedupe, pre-fusion). */
475
493
  chunkCount: number;
494
+ /**
495
+ * Memories actually RETURNED — `memories.length` after fusion, cross-lane
496
+ * dedup and the `limit` slice.
497
+ *
498
+ * `candidateCount` is what was considered; this is what the caller got, and
499
+ * the two are routinely far apart (the fact lane pulls `limit * 2` when fusing).
500
+ * Every consumer that wanted "how many memories did this turn actually get"
501
+ * was reading `candidateCount` and overcounting.
502
+ */
503
+ admittedCount: number;
504
+ /** Highest score among the returned memories; -1 when none were returned. */
505
+ topScore: number;
506
+ /** Lowest score among the returned memories; -1 when none were returned. */
507
+ lowestAdmittedScore: number;
508
+ /**
509
+ * The similarity floor the lane that produced these scores actually applied:
510
+ * the fact lane's when it RETURNED results, otherwise the chunk lane's, and
511
+ * **-1 when neither ran** (empty query, unwired context). Gated on results
512
+ * rather than on the lane running, because a fact lane that ran and came
513
+ * back empty filtered none of the scores in the payload — reporting its
514
+ * 0.1 default against chunks that cleared 0.5 corrupted the telemetry.
515
+ * When NOTHING was admitted, the floor a lane did apply is still reported:
516
+ * "searched at this floor, found nothing" is the useful reading.
517
+ *
518
+ * Per-lane rather than one constant because the two defaults differ (0.1 fact
519
+ * / 0.5 chunk), so a single seeded value reported a floor that a chunk-only
520
+ * recall never applied. Reported next to the scores because the scores alone
521
+ * cannot say what they cleared.
522
+ */
523
+ minScoreApplied: number;
524
+ /**
525
+ * Whether the `limit` cut an ELIGIBLE result — recorded at the cut, not
526
+ * derived from `candidateCount > limit`. In the fused path `candidateCount`
527
+ * counts before provenance suppression, so a recall whose suppressed chunks
528
+ * brought it under the limit would otherwise report a truncation that never
529
+ * happened.
530
+ */
531
+ truncated: boolean;
532
+ /** Memory ids the W5 graph (entity) side lane contributed to the fusion. */
533
+ graphLaneCount: number;
534
+ /** Memory ids the W6 temporal side lane contributed to the fusion. */
535
+ temporalLaneCount: number;
536
+ /** Why nothing came back — see {@link RecallEmptyReason}. `""` when something did. */
537
+ emptyReason: RecallEmptyReason;
476
538
  /** Wall-clock phase timings (ms). */
477
539
  timings: {
478
540
  /** Whole `recall()` call. */
@@ -185,6 +185,23 @@ function createRecallDiagnosticsHandler(sink) {
185
185
  usedBudget: diagnostics.usedBudget,
186
186
  reranked: diagnostics.reranked,
187
187
  candidateCount: diagnostics.candidateCount,
188
+ // What the caller actually GOT. `candidateCount` is what was considered,
189
+ // and the two are routinely far apart — the fact lane pulls `limit * 2`
190
+ // when fusing — so a consumer reading candidateCount as "memories this
191
+ // turn received" overcounts.
192
+ admittedCount: diagnostics.admittedCount,
193
+ // Both ends of the admitted range, and the lane counts, ride the EVENT as
194
+ // well as the metrics: a track-only sink is a supported configuration
195
+ // (the module's own PostHog example is exactly that shape), and it could
196
+ // otherwise see neither the bottom of the score range nor which side lane
197
+ // contributed.
198
+ topScore: diagnostics.topScore,
199
+ lowestAdmittedScore: diagnostics.lowestAdmittedScore,
200
+ minScoreApplied: diagnostics.minScoreApplied,
201
+ truncated: diagnostics.truncated,
202
+ emptyReason: diagnostics.emptyReason,
203
+ graphLaneCount: diagnostics.graphLaneCount,
204
+ temporalLaneCount: diagnostics.temporalLaneCount,
188
205
  factCount: diagnostics.factCount,
189
206
  chunkCount: diagnostics.chunkCount,
190
207
  totalMs: diagnostics.timings.total,
@@ -198,8 +215,11 @@ function createRecallDiagnosticsHandler(sink) {
198
215
  metric("recall.duration", ms, { lane });
199
216
  }
200
217
  metric("recall.candidates", diagnostics.candidateCount, {});
218
+ metric("recall.admitted", diagnostics.admittedCount, {});
201
219
  metric("recall.facts", diagnostics.factCount, {});
202
220
  metric("recall.chunks", diagnostics.chunkCount, {});
221
+ metric("recall.lane.graph", diagnostics.graphLaneCount, {});
222
+ metric("recall.lane.temporal", diagnostics.temporalLaneCount, {});
203
223
  if (diagnostics.vaultSize !== void 0) {
204
224
  metric("recall.vault.size", diagnostics.vaultSize, {});
205
225
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anuma/sdk",
3
- "version": "6.19.2",
3
+ "version": "6.21.0",
4
4
  "description": "",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",