@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.
- package/dist/expo/{chunk-7HRJOHW4.mjs → chunk-CIMZZ7GQ.mjs} +59 -12
- package/dist/expo/index.cjs +142 -43
- package/dist/expo/index.d.mts +171 -2
- package/dist/expo/index.d.ts +171 -2
- package/dist/expo/index.mjs +85 -33
- package/dist/expo/{recall-SCMH2R24.mjs → recall-FQQGE6A2.mjs} +1 -1
- package/dist/react/{chunk-M53NXLZS.mjs → chunk-JRVAUZHM.mjs} +59 -12
- package/dist/react/index.cjs +142 -43
- package/dist/react/index.d.mts +171 -2
- package/dist/react/index.d.ts +171 -2
- package/dist/react/index.mjs +85 -33
- package/dist/react/{recall-B3M4BWQ2.mjs → recall-7Q3V337H.mjs} +1 -1
- package/dist/server/{chunk-X5EZQFGR.mjs → chunk-4DSDATYG.mjs} +59 -12
- package/dist/server/index.cjs +142 -43
- package/dist/server/index.d.mts +171 -2
- package/dist/server/index.d.ts +171 -2
- package/dist/server/index.mjs +85 -33
- package/dist/server/{recall-M3KBKU74.mjs → recall-HA4UX4BB.mjs} +1 -1
- package/dist/telemetry/index.cjs +20 -0
- package/dist/telemetry/index.d.mts +63 -1
- package/dist/telemetry/index.d.ts +63 -1
- package/dist/telemetry/index.mjs +20 -0
- package/package.json +1 -1
package/dist/server/index.mjs
CHANGED
|
@@ -141,7 +141,7 @@ import {
|
|
|
141
141
|
updateVaultMemoryOp,
|
|
142
142
|
upsertMessageOp,
|
|
143
143
|
vaultMemoryToStored
|
|
144
|
-
} from "./chunk-
|
|
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
|
-
|
|
5527
|
-
|
|
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
|
|
5602
|
+
const tombstone = await findTombstoneMatch(embedding, embeddingModel, resolvedScope, ctx, {
|
|
5591
5603
|
threshold,
|
|
5592
5604
|
folderId: options.folderId
|
|
5593
5605
|
});
|
|
5594
|
-
if (
|
|
5595
|
-
return {
|
|
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
|
-
|
|
6420
|
-
|
|
6421
|
-
{
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
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.
|
package/dist/telemetry/index.cjs
CHANGED
|
@@ -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. */
|
package/dist/telemetry/index.mjs
CHANGED
|
@@ -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
|
}
|