@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.
@@ -7962,7 +7962,22 @@ async function recall(query, ctx, options = {}) {
7962
7962
  let factLaneRankedOnCosine = false;
7963
7963
  let decryptLastRan;
7964
7964
  let vaultRowsDecrypted;
7965
- const emitDiagnostics = (candidateCount) => {
7965
+ let graphLaneCount = 0;
7966
+ let temporalLaneCount = 0;
7967
+ let graphLaneFailed = false;
7968
+ let temporalLaneFailed = false;
7969
+ let hitLimit = false;
7970
+ let factFloor = -1;
7971
+ let chunkFloor = -1;
7972
+ const emptyReasonFor = (admitted, laneRan) => {
7973
+ if (admitted > 0) return "";
7974
+ if (!query || typeof query !== "string" || query.trim().length === 0) return "empty-query";
7975
+ if (!laneRan) return "no-lanes";
7976
+ const chunkLaneCouldAnswer = types.includes("chunk") && !!ctx.storageCtx;
7977
+ if (vaultSize === 0 && !chunkLaneCouldAnswer) return "vault-empty";
7978
+ return "no-candidates";
7979
+ };
7980
+ const emitDiagnostics = (candidateCount, admitted) => {
7966
7981
  const cb = options.onDiagnostics;
7967
7982
  if (!cb) return;
7968
7983
  const degraded = [];
@@ -7973,6 +7988,11 @@ async function recall(query, ctx, options = {}) {
7973
7988
  degraded.push("decompose-moved");
7974
7989
  }
7975
7990
  if (embeddingsUnavailable) degraded.push("embeddings-unavailable");
7991
+ if (graphLaneFailed) degraded.push("graph-lane-failed");
7992
+ if (temporalLaneFailed) degraded.push("temporal-lane-failed");
7993
+ const laneRan = types.includes("fact") && !!ctx.vaultCtx && !!ctx.vaultCache || types.includes("chunk") && !!ctx.storageCtx;
7994
+ const scores = admitted.map((m) => m.score);
7995
+ const minScoreApplied = factResults.length > 0 ? factFloor : chunkResults.length > 0 ? chunkFloor : factFloor >= 0 ? factFloor : chunkFloor;
7976
7996
  const diagnostics = {
7977
7997
  usedBudget,
7978
7998
  reranked: didRerank,
@@ -7983,6 +8003,14 @@ async function recall(query, ctx, options = {}) {
7983
8003
  ...vaultRowsEmbedded !== void 0 && { vaultRowsEmbedded },
7984
8004
  factCount: factResults.length,
7985
8005
  chunkCount: chunkResults.length,
8006
+ admittedCount: admitted.length,
8007
+ topScore: scores.length > 0 ? Math.max(...scores) : -1,
8008
+ lowestAdmittedScore: scores.length > 0 ? Math.min(...scores) : -1,
8009
+ minScoreApplied,
8010
+ truncated: hitLimit,
8011
+ graphLaneCount,
8012
+ temporalLaneCount,
8013
+ emptyReason: emptyReasonFor(admitted.length, laneRan),
7986
8014
  timings: {
7987
8015
  total: nowMs() - t0,
7988
8016
  prep: prepMs,
@@ -8000,7 +8028,7 @@ async function recall(query, ctx, options = {}) {
8000
8028
  }
8001
8029
  };
8002
8030
  if (!query || typeof query !== "string" || query.trim().length === 0) {
8003
- emitDiagnostics(0);
8031
+ emitDiagnostics(0, []);
8004
8032
  return { memories: [], usedBudget, reranked: false, candidateCount: 0 };
8005
8033
  }
8006
8034
  const needsChunkEmbedding = types.includes("chunk") && ctx.storageCtx;
@@ -8034,6 +8062,7 @@ async function recall(query, ctx, options = {}) {
8034
8062
  // empty ranking instead (mirrors safeCountVault's fail-soft posture).
8035
8063
  safeLane(
8036
8064
  "graph",
8065
+ () => graphLaneFailed = true,
8037
8066
  () => buildGraphLaneRanking(query, ctx, flags.traverse, {
8038
8067
  ...options.maxHops !== void 0 && { maxHops: options.maxHops },
8039
8068
  ...options.entityFanout !== void 0 && { entityFanout: options.entityFanout },
@@ -8042,12 +8071,19 @@ async function recall(query, ctx, options = {}) {
8042
8071
  ...graphRefiner && { refineNeighbors: graphRefiner }
8043
8072
  })
8044
8073
  ),
8045
- wantsTemporal ? safeLane("temporal", () => buildTemporalLaneRanking(query, ctx.vaultCtx, options.now)) : Promise.resolve([])
8074
+ wantsTemporal ? safeLane(
8075
+ "temporal",
8076
+ () => temporalLaneFailed = true,
8077
+ () => buildTemporalLaneRanking(query, ctx.vaultCtx, options.now)
8078
+ ) : Promise.resolve([])
8046
8079
  ]);
8047
8080
  prepMs = nowMs() - prepStart;
8081
+ graphLaneCount = entityRanking.length;
8082
+ temporalLaneCount = temporalRanking.length;
8048
8083
  if (types.includes("fact") && ctx.vaultCtx && ctx.vaultCache) {
8049
8084
  const factStart = nowMs();
8050
8085
  const vaultMinScore = options.minScore ?? DEFAULT_FACT_MIN_SCORE;
8086
+ factFloor = vaultMinScore;
8051
8087
  const {
8052
8088
  results,
8053
8089
  vaultSize: size,
@@ -8127,6 +8163,7 @@ async function recall(query, ctx, options = {}) {
8127
8163
  if (types.includes("chunk") && ctx.storageCtx && queryEmbedding) {
8128
8164
  const chunkStart = nowMs();
8129
8165
  const chunkMinScore = options.minScore ?? DEFAULT_CHUNK_MIN_SCORE;
8166
+ chunkFloor = chunkMinScore;
8130
8167
  const results = await searchChunksOp(ctx.storageCtx, queryEmbedding, {
8131
8168
  limit: types.includes("fact") ? Math.max(limit * 2, 16) : limit,
8132
8169
  minSimilarity: chunkMinScore,
@@ -8153,9 +8190,11 @@ async function recall(query, ctx, options = {}) {
8153
8190
  memories2.sort((a, b) => b.score - a.score);
8154
8191
  const candidateCount = factResults.length + chunkResults.length;
8155
8192
  fuseMs = nowMs() - fuseStart;
8156
- emitDiagnostics(candidateCount);
8193
+ const admitted = memories2.slice(0, limit);
8194
+ hitLimit = memories2.length > limit;
8195
+ emitDiagnostics(candidateCount, admitted);
8157
8196
  return {
8158
- memories: memories2.slice(0, limit),
8197
+ memories: admitted,
8159
8198
  usedBudget,
8160
8199
  reranked: didRerank,
8161
8200
  candidateCount,
@@ -8185,12 +8224,16 @@ async function recall(query, ctx, options = {}) {
8185
8224
  const ordered = [...byId.values()].sort((a, b) => b.score - a.score);
8186
8225
  const selectWith = (suppressed2) => {
8187
8226
  const out = [];
8227
+ let cut = false;
8188
8228
  for (const m of ordered) {
8189
- if (out.length >= limit) break;
8190
8229
  if (m.kind === "chunk" && m.messageId && suppressed2.has(m.messageId)) continue;
8230
+ if (out.length >= limit) {
8231
+ cut = true;
8232
+ break;
8233
+ }
8191
8234
  out.push(m);
8192
8235
  }
8193
- return out;
8236
+ return { out, cut };
8194
8237
  };
8195
8238
  const provenanceOf = (selected) => {
8196
8239
  const s = /* @__PURE__ */ new Set();
@@ -8200,15 +8243,18 @@ async function recall(query, ctx, options = {}) {
8200
8243
  return s;
8201
8244
  };
8202
8245
  let suppressed = /* @__PURE__ */ new Set();
8203
- let memories = selectWith(suppressed);
8246
+ let selection = selectWith(suppressed);
8247
+ let memories = selection.out;
8204
8248
  for (let i = 0; i < ordered.length; i++) {
8205
8249
  const next = provenanceOf(memories);
8206
8250
  if (next.size === suppressed.size && [...next].every((id) => suppressed.has(id))) break;
8207
8251
  suppressed = next;
8208
- memories = selectWith(suppressed);
8252
+ selection = selectWith(suppressed);
8253
+ memories = selection.out;
8209
8254
  }
8255
+ hitLimit = selection.cut;
8210
8256
  fuseMs = nowMs() - fuseStart;
8211
- emitDiagnostics(byId.size);
8257
+ emitDiagnostics(byId.size, memories);
8212
8258
  return {
8213
8259
  memories,
8214
8260
  usedBudget,
@@ -8292,10 +8338,11 @@ async function safeCountVault(ctx) {
8292
8338
  return void 0;
8293
8339
  }
8294
8340
  }
8295
- async function safeLane(label, run) {
8341
+ async function safeLane(label, onFailure, run) {
8296
8342
  try {
8297
8343
  return await run();
8298
8344
  } catch (err) {
8345
+ onFailure();
8299
8346
  getLogger().warn(
8300
8347
  `[memory/recall] ${label} lane failed; continuing without it: ${err instanceof Error ? err.message : String(err)}`
8301
8348
  );
@@ -17683,6 +17730,7 @@ async function retain(content, ctx, options = {}) {
17683
17730
  const threshold = options.autoMergeThreshold ?? DEFAULT_AUTO_MERGE_THRESHOLD;
17684
17731
  const resolvedScope = options.scope ?? DEFAULT_SCOPE;
17685
17732
  let supersedeTargetIds = [];
17733
+ let consolidationDecidedCreate = false;
17686
17734
  let supersedeContent;
17687
17735
  let prepared;
17688
17736
  if (enableAutoMerge) {
@@ -17710,8 +17758,12 @@ async function retain(content, ctx, options = {}) {
17710
17758
  const outcome = await tryConsolidate(trimmed, ctx, options, prepared);
17711
17759
  if (outcome) {
17712
17760
  if ("done" in outcome) return outcome.done;
17713
- supersedeTargetIds = outcome.supersede;
17714
- supersedeContent = outcome.content;
17761
+ if ("create" in outcome) {
17762
+ consolidationDecidedCreate = true;
17763
+ } else {
17764
+ supersedeTargetIds = outcome.supersede;
17765
+ supersedeContent = outcome.content;
17766
+ }
17715
17767
  }
17716
17768
  }
17717
17769
  if (supersedeTargetIds.length === 0) {
@@ -17761,7 +17813,14 @@ async function retain(content, ctx, options = {}) {
17761
17813
  action: "merge",
17762
17814
  memoryId: targetId,
17763
17815
  targetId,
17764
- proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1
17816
+ proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1,
17817
+ similarity: matches[0].similarity,
17818
+ // Stage 1 may have run and explicitly said `create`, and Stage 2
17819
+ // then found a strict-cosine match anyway. Reporting the decision
17820
+ // here is what makes that DISAGREEMENT visible — without it the
17821
+ // model's create silently vanished from the distribution whenever
17822
+ // the cosine stage won.
17823
+ ...consolidationDecidedCreate && { consolidation: "create" }
17765
17824
  };
17766
17825
  }
17767
17826
  await assertMergeTargetGoneOrThrow(ctx, targetId);
@@ -17774,12 +17833,18 @@ async function retain(content, ctx, options = {}) {
17774
17833
  const embedding = reusableQueryEmbedding ?? await generateEmbedding(contentToWrite, ctx.embeddingOptions);
17775
17834
  const embeddingModel = ctx.embeddingOptions.model ?? DEFAULT_API_EMBEDDING_MODEL;
17776
17835
  if (options.respectTombstones) {
17777
- const tombstoneId = await findTombstoneMatch(embedding, embeddingModel, resolvedScope, ctx, {
17836
+ const tombstone = await findTombstoneMatch(embedding, embeddingModel, resolvedScope, ctx, {
17778
17837
  threshold,
17779
17838
  folderId: options.folderId
17780
17839
  });
17781
- if (tombstoneId) {
17782
- return { action: "suppressed", memoryId: tombstoneId, tombstoneId, proofCount: 0 };
17840
+ if (tombstone) {
17841
+ return {
17842
+ action: "suppressed",
17843
+ memoryId: tombstone.id,
17844
+ tombstoneId: tombstone.id,
17845
+ proofCount: 0,
17846
+ similarity: tombstone.similarity
17847
+ };
17783
17848
  }
17784
17849
  }
17785
17850
  const createOpts = {
@@ -17846,7 +17911,8 @@ async function retain(content, ctx, options = {}) {
17846
17911
  action: "supersede",
17847
17912
  memoryId: created2.uniqueId,
17848
17913
  targetId: primaryTargetId,
17849
- proofCount: 1
17914
+ proofCount: 1,
17915
+ consolidation: "supersede"
17850
17916
  };
17851
17917
  }
17852
17918
  notifyConsolidationFallback(
@@ -17860,7 +17926,8 @@ async function retain(content, ctx, options = {}) {
17860
17926
  return {
17861
17927
  action: "create",
17862
17928
  memoryId: created.uniqueId,
17863
- proofCount: 1
17929
+ proofCount: 1,
17930
+ ...consolidationDecidedCreate && { consolidation: "create" }
17864
17931
  };
17865
17932
  }
17866
17933
  function unionStrings(a, b) {
@@ -17890,7 +17957,7 @@ async function findTombstoneMatch(embedding, embeddingModel, scope, ctx, opts) {
17890
17957
  bestId = row.uniqueId;
17891
17958
  }
17892
17959
  }
17893
- return bestId;
17960
+ return bestId === null ? null : { id: bestId, similarity: bestSim };
17894
17961
  }
17895
17962
  function pickEventTimeUpdate(existing, incoming) {
17896
17963
  if (incoming === void 0 || incoming === null) return void 0;
@@ -17932,7 +17999,7 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
17932
17999
  }));
17933
18000
  const { consolidateMemory: doConsolidate } = await Promise.resolve().then(() => (init_consolidate(), consolidate_exports));
17934
18001
  const decision = await doConsolidate(trimmed, candidates, consolidateOptions);
17935
- if (decision.action === "create") return null;
18002
+ if (decision.action === "create") return decision.fallbackReason ? null : { create: true };
17936
18003
  if (decision.action === "supersede" && decision.content) {
17937
18004
  const requestedIds = decision.targetIds?.length ? decision.targetIds : decision.targetId ? [decision.targetId] : [];
17938
18005
  if (requestedIds.length === 0) return null;
@@ -17991,7 +18058,8 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
17991
18058
  action: "merge",
17992
18059
  memoryId: decision.targetId,
17993
18060
  targetId: decision.targetId,
17994
- proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1
18061
+ proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1,
18062
+ consolidation: "noop"
17995
18063
  }
17996
18064
  };
17997
18065
  }
@@ -18044,7 +18112,8 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
18044
18112
  action: "update",
18045
18113
  memoryId: decision.targetId,
18046
18114
  targetId: decision.targetId,
18047
- proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1
18115
+ proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1,
18116
+ consolidation: "update"
18048
18117
  }
18049
18118
  };
18050
18119
  }
@@ -18186,6 +18255,11 @@ Extract durable user facts.`,
18186
18255
  fallbackSourceId,
18187
18256
  options.userIdentity ?? []
18188
18257
  );
18258
+ const rawCandidates = parsed.candidates;
18259
+ options.onCandidatesParsed?.({
18260
+ rawCount: Array.isArray(rawCandidates) ? rawCandidates.length : 0,
18261
+ validCount: candidates.length
18262
+ });
18189
18263
  if (!redactor) return candidates;
18190
18264
  const restored = restoreCandidates(candidates, redactor, options.userIdentity ?? []);
18191
18265
  if (candidates.length > 0 && restored.length === 0) {
@@ -18222,8 +18296,12 @@ async function extractAndRetain(messages, retainCtx, options) {
18222
18296
  let exhaustedEmpty = false;
18223
18297
  let exhaustedFailure;
18224
18298
  let droppedAfterRedaction = false;
18299
+ let rawCandidateCount = 0;
18300
+ let validCandidateCount = 0;
18225
18301
  const callerOnExhaustedEmpty = options.extract.onExhaustedEmpty;
18226
18302
  const callerOnCandidatesDropped = options.extract.onCandidatesDropped;
18303
+ const callerOnCandidatesParsed = options.extract.onCandidatesParsed;
18304
+ const tExtract = Date.now();
18227
18305
  const candidates = await extractFacts(messages, {
18228
18306
  ...options.extract,
18229
18307
  onExhaustedEmpty: (failure) => {
@@ -18234,8 +18312,14 @@ async function extractAndRetain(messages, retainCtx, options) {
18234
18312
  onCandidatesDropped: () => {
18235
18313
  droppedAfterRedaction = true;
18236
18314
  callerOnCandidatesDropped?.();
18315
+ },
18316
+ onCandidatesParsed: (stats) => {
18317
+ rawCandidateCount = stats.rawCount;
18318
+ validCandidateCount = stats.validCount;
18319
+ callerOnCandidatesParsed?.(stats);
18237
18320
  }
18238
18321
  });
18322
+ const extractMs = Date.now() - tExtract;
18239
18323
  const filtered = candidates.filter((c) => c.confidence >= minConfidence);
18240
18324
  const log = getLogger();
18241
18325
  const screened = screenCandidatesForInjection(filtered);
@@ -18283,6 +18367,7 @@ async function extractAndRetain(messages, retainCtx, options) {
18283
18367
  const results = [];
18284
18368
  const quarantinedInfo = [];
18285
18369
  let failedWrites = 0;
18370
+ const tRetain = Date.now();
18286
18371
  for (const { candidate, isQuarantined, reason, signature } of toRetain) {
18287
18372
  try {
18288
18373
  const result = await retain(candidate.content, retainCtx, {
@@ -18349,6 +18434,21 @@ async function extractAndRetain(messages, retainCtx, options) {
18349
18434
  failedCount: failedWrites,
18350
18435
  outcome,
18351
18436
  quarantined: quarantinedInfo,
18437
+ funnel: {
18438
+ rawCandidateCount,
18439
+ validCandidateCount,
18440
+ afterRedactionCount: candidates.length,
18441
+ aboveConfidenceCount: filtered.length,
18442
+ // The PERSISTED quarantines (`quarantinedInfo`), not the screened list: a
18443
+ // screened candidate whose retain() throws is counted in `failedCount`,
18444
+ // and counting it here too would double it and break the identity below.
18445
+ // This also keeps the count equal to `quarantined.length` on the result.
18446
+ quarantinedCount: quarantinedInfo.length,
18447
+ retainedCount: results.length,
18448
+ failedCount: failedWrites
18449
+ },
18450
+ timings: { extractMs, retainMs: Date.now() - tRetain },
18451
+ model: options.extract.model ?? DEFAULT_EXTRACTION_MODEL,
18352
18452
  // Only meaningful alongside `outcome: "empty-after-retry"`. Returned as well
18353
18453
  // as pushed through the callback so a consumer that only inspects the result
18354
18454
  // (rather than wiring a hook) can still report WHY.
@@ -18604,27 +18704,23 @@ function createAutoExtractor(options) {
18604
18704
  const t0 = Date.now();
18605
18705
  void (async () => {
18606
18706
  try {
18607
- const { candidates, results, failedCount, outcome, failure } = await extractAndRetain(
18608
- window2,
18609
- retainCtx,
18610
- {
18611
- extract,
18612
- ...options.minConfidence !== void 0 && { minConfidence: options.minConfidence },
18613
- ...options.entityCtx !== void 0 && { entityCtx: options.entityCtx },
18614
- ...options.scope !== void 0 && { scope: options.scope },
18615
- ...options.folderId !== void 0 && { folderId: options.folderId },
18616
- ...consolidateOptions !== void 0 && { consolidateOptions },
18617
- ...injectionClassifierOptions !== void 0 && {
18618
- injectionClassifier: injectionClassifierOptions
18619
- },
18620
- ...options.onCandidateFailed && {
18621
- onCandidateFailed: (candidate, error) => options.onCandidateFailed?.({ candidate, error, conversationId })
18622
- },
18623
- ...options.onMemoryQuarantined && {
18624
- onQuarantined: (info) => options.onMemoryQuarantined?.({ ...info, conversationId })
18625
- }
18707
+ const { candidates, results, failedCount, outcome, failure, funnel, timings, model } = await extractAndRetain(window2, retainCtx, {
18708
+ extract,
18709
+ ...options.minConfidence !== void 0 && { minConfidence: options.minConfidence },
18710
+ ...options.entityCtx !== void 0 && { entityCtx: options.entityCtx },
18711
+ ...options.scope !== void 0 && { scope: options.scope },
18712
+ ...options.folderId !== void 0 && { folderId: options.folderId },
18713
+ ...consolidateOptions !== void 0 && { consolidateOptions },
18714
+ ...injectionClassifierOptions !== void 0 && {
18715
+ injectionClassifier: injectionClassifierOptions
18716
+ },
18717
+ ...options.onCandidateFailed && {
18718
+ onCandidateFailed: (candidate, error) => options.onCandidateFailed?.({ candidate, error, conversationId })
18719
+ },
18720
+ ...options.onMemoryQuarantined && {
18721
+ onQuarantined: (info) => options.onMemoryQuarantined?.({ ...info, conversationId })
18626
18722
  }
18627
- );
18723
+ });
18628
18724
  if (outcome !== "empty-after-retry") {
18629
18725
  const advancedTo = window2[window2.length - 1].id;
18630
18726
  stateFor(conversationId).watermark = advancedTo;
@@ -18645,6 +18741,9 @@ function createAutoExtractor(options) {
18645
18741
  durationMs: Date.now() - t0,
18646
18742
  conversationId,
18647
18743
  outcome,
18744
+ funnel,
18745
+ timings,
18746
+ model,
18648
18747
  // Only set alongside `outcome: "empty-after-retry"`. Spread so the key
18649
18748
  // is absent rather than explicitly undefined on a healthy turn —
18650
18749
  // analytics backends store an explicit undefined as a real value.