@anuma/sdk 6.10.0 → 6.12.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.
@@ -6140,6 +6140,51 @@ declare function getMemoriesByEntityNamesOp(ctx: EntityOperationsContext, entity
6140
6140
  */
6141
6141
  declare function getEntitiesByMemoryIdsOp(ctx: EntityOperationsContext, memoryIds: readonly string[]): Promise<Map<string, Set<string>>>;
6142
6142
 
6143
+ /**
6144
+ * Class-B task types — the vocabulary of the `X-Anuma-Task-Type` header.
6145
+ *
6146
+ * A Class-B flow has ONE fixed purpose and no conversation: extract memories
6147
+ * from a snippet, pick a topic, consolidate duplicates, classify a query. Today
6148
+ * each of those builds its own system prompt on the client and sends it as a
6149
+ * `role:system` message, which makes the prompt a suggestion rather than a
6150
+ * contract — a caller can reword it into "you are a general assistant" and get
6151
+ * general chat at internal-endpoint prices.
6152
+ *
6153
+ * The fix is that the SERVER owns the text and the client only names the task.
6154
+ * This header is the name. The portal allowlists it against its own copy of this
6155
+ * enum (ai-portal `internal/systemprompt/tasks.go`) and DROPS anything it does
6156
+ * not recognise, so an unknown value degrades to "no task declared" rather than
6157
+ * failing the request.
6158
+ *
6159
+ * ⚠ These string values are a wire contract with ai-portal. Adding one here
6160
+ * before the portal knows it is harmless (it is dropped); renaming or removing
6161
+ * one silently stops that flow being counted. Keep them in step with
6162
+ * `AllTaskTypes` on the portal side, and see ai-portal
6163
+ * `docs/BACKEND_OWNED_TASK_PROMPTS.md` for the rollout.
6164
+ *
6165
+ * ⚠ RELEASE ORDER. This union is a PUBLIC type, so a split is breaking in both
6166
+ * directions and has to be sequenced:
6167
+ *
6168
+ * 1. merge + publish this package;
6169
+ * 2. bump `@anuma/sdk` in the consumer and switch its call sites to the new
6170
+ * names in the same commit.
6171
+ *
6172
+ * A consumer on the OLD version cannot compile the new names, and a consumer on
6173
+ * the NEW version cannot compile the retired ones (`app_inspiration` is gone
6174
+ * here, replaced by the three `app_inspiration_*` names). Both halves of that
6175
+ * are type-only — the wire itself always degrades safely, because the portal
6176
+ * drops any value it does not recognise. Concretely: ai-memoryless-client#5782
6177
+ * is blocked on this publish for exactly that reason.
6178
+ *
6179
+ * Deliberately ABSENT: agent personas, `reflect()` when it answers the user, and
6180
+ * the app-idea chat. Those carry an open conversation with the user, so a fixed
6181
+ * server-owned prompt would be wrong rather than merely premature — the same
6182
+ * boundary {@link INTERNAL_FLOW_MARKER} draws.
6183
+ *
6184
+ * @public
6185
+ */
6186
+ type TaskType = "title" | "commit_message" | "memory_extract" | "memory_dedup" | "memory_decay" | "memory_injection_check" | "memory_verify_support" | "memory_autosort" | "memory_topic" | "memory_consolidate" | "memory_decompose" | "memory_profile_synth" | "memory_graph" | "folder_context" | "selection_memory" | "summarize" | "style_analysis" | "classify_search" | "app_inspiration_generate" | "app_inspiration_trends" | "app_inspiration_remix" | "spotlight_image" | "slide_image" | "slide_video" | "slide_music" | "slide_sfx" | "media_video" | "media_audio" | "media_bg_removal" | "media_image_preset" | "tool_app_builder" | "tool_document_builder" | "tool_slides" | "tool_slides_image_gen" | "connector_guidance" | "cf_task" | "xbot_intent" | "xbot_council" | "scheduled_task";
6187
+
6143
6188
  /**
6144
6189
  * Shared portal LLM call for the memory pipeline.
6145
6190
  *
@@ -6992,6 +7037,17 @@ interface RecallDiagnostics {
6992
7037
  * never the cost.
6993
7038
  */
6994
7039
  vaultRowsDecrypted?: number;
7040
+ /**
7041
+ * Rows the fact lane had to re-embed through the portal because their stored
7042
+ * vector was unusable — stale `embedding_model`, wrong dimension, or
7043
+ * unparseable. Absent when the fact lane didn't run.
7044
+ *
7045
+ * Expected to be 0 on a healthy vault: rows are embedded at write time and the
7046
+ * re-embed writes the current model back, so a persistently non-zero value
7047
+ * means the writeback is not sticking and every turn is paying for it. On the
7048
+ * LEGACY read path this batch is uncapped, so it can be the whole vault.
7049
+ */
7050
+ vaultRowsEmbedded?: number;
6995
7051
  /** Facts the fact lane returned (post-dedupe, pre-fusion). */
6996
7052
  factCount: number;
6997
7053
  /** Chunks the chunk lane returned (post-dedupe, pre-fusion). */
@@ -7004,6 +7060,37 @@ interface RecallDiagnostics {
7004
7060
  prep: number;
7005
7061
  /** Vault fact-lane search (`searchVaultMemoriesWithSize`). */
7006
7062
  factLane: number;
7063
+ /**
7064
+ * The cross-encoder's share of {@link factLane} — wall-clock spent inside
7065
+ * `rerankPairs`. Billed even when the CE threw partway through, because a
7066
+ * rerank that burned three seconds and then failed still cost them.
7067
+ *
7068
+ * A SUBSET of `factLane`, not a sibling: read `factLane - rerank` for
7069
+ * everything else the lane did (query embed, vault read, fused ranking).
7070
+ * 0 when the CE did not run; `reranked` is what distinguishes that from a
7071
+ * rerank that cost nothing.
7072
+ *
7073
+ * Exists because #845 spent three rounds arguing about which stage inside
7074
+ * the fact lane dominated — whole-vault read, then admission window, then
7075
+ * the CE — with one aggregate number for all of them. Every hypothesis was
7076
+ * an inference; this makes the question a query.
7077
+ */
7078
+ rerank: number;
7079
+ /**
7080
+ * The query embed's share of {@link factLane} — one portal round trip.
7081
+ *
7082
+ * Also a SUBSET of `factLane`. Together with `rerank` this makes the lane
7083
+ * decomposable: `factLane - queryEmbed - rerank` is the local work (vault
7084
+ * read, decrypt, BM25/cosine fusion), and each of the three implies a
7085
+ * completely different fix.
7086
+ *
7087
+ * 0 when the lane returned before embedding, which includes every empty
7088
+ * vault — `prepareVaultCandidates` short-circuits first. That is why the
7089
+ * fast `vault_size = 0` population never established a baseline for this
7090
+ * cost, and why the ~850ms floor on the smallest NON-empty vaults had no
7091
+ * attributable owner.
7092
+ */
7093
+ queryEmbed: number;
7007
7094
  /** Chunk-lane search (`searchChunksOp`). */
7008
7095
  chunkLane: number;
7009
7096
  /** Cross-lane RRF fusion + provenance dedup after both lanes. */
@@ -9348,6 +9435,28 @@ interface ReflectOptions extends RecallOptions, PortalLlmAuth {
9348
9435
  maxTokens?: number;
9349
9436
  /** Override the grounding system prompt. */
9350
9437
  systemPrompt?: string;
9438
+ /**
9439
+ * Extra caller instruction to carry on the USER turn, between the question and
9440
+ * the evidence block (see the `userMessage` assembly below). This is the slot a
9441
+ * background caller uses to keep its per-request data OUT of the system message
9442
+ * without colliding with the numbered evidence list — profile-facet synthesis
9443
+ * puts its section label, guidance and response-field hint here so its system
9444
+ * half can stay fixed and server-ownable.
9445
+ */
9446
+ userInstructions?: string;
9447
+ /**
9448
+ * Class-B task name for the `X-Anuma-Task-Type` header, or nothing.
9449
+ *
9450
+ * Deliberately OPTIONAL and unset by default. reflect() also answers the user's
9451
+ * OWN question, and that traffic is chat, not an internal flow — declaring a
9452
+ * task type unconditionally here would put an internal-flow name on real
9453
+ * conversation, which is the same boundary `INTERNAL_FLOW_MARKER` draws
9454
+ * (reflect is deliberately unmarked; its background caller marks its own
9455
+ * prompt — see ../internalFlowMarker.ts). So the name is per call: only a
9456
+ * caller with ONE fixed purpose passes
9457
+ * one, and today that is profile-facet synthesis (`memory_profile_synth`).
9458
+ */
9459
+ taskType?: TaskType;
9351
9460
  /** Endpoint for the answer LLM. */
9352
9461
  baseUrl?: string;
9353
9462
  /** Override fetch (for tests). */
@@ -178,7 +178,7 @@ import {
178
178
  updateVaultMemoryOp,
179
179
  useEncryption,
180
180
  vaultMemoryToStored
181
- } from "./chunk-EJ65X4GI.mjs";
181
+ } from "./chunk-6IBFFWCE.mjs";
182
182
  import {
183
183
  PiiRedactor,
184
184
  createStreamingDeAnonymizer,
@@ -8949,11 +8949,13 @@ Respond with ONLY a single JSON object conforming to this JSON Schema, with no p
8949
8949
  ${JSON.stringify(
8950
8950
  options.responseSchema
8951
8951
  )}` : basePrompt;
8952
- const userMessage = `Question:
8953
- ${trimmed}
8954
-
8955
- Memories (use only these as evidence):
8956
- ${evidence}`;
8952
+ const userMessage = [
8953
+ `Question:
8954
+ ${trimmed}`,
8955
+ ...options.userInstructions ? [options.userInstructions] : [],
8956
+ `Memories (use only these as evidence):
8957
+ ${evidence}`
8958
+ ].join("\n\n");
8957
8959
  const maxTokens = options.maxTokens ?? DEFAULT_MAX_TOKENS;
8958
8960
  const fetchImpl = options.fetchFn ?? fetch;
8959
8961
  const log = getLogger();
@@ -8967,7 +8969,13 @@ ${evidence}`;
8967
8969
  try {
8968
8970
  response = await fetchImpl(`${baseUrl}/api/v1/chat/completions`, {
8969
8971
  method: "POST",
8970
- headers: { ...authHeaders, "Content-Type": "application/json" },
8972
+ // No task type unless the caller named one — an undeclared task adds no
8973
+ // header at all, which is what keeps user-facing reflect() unlabelled.
8974
+ headers: {
8975
+ ...authHeaders,
8976
+ ...taskTypeHeader(options.taskType),
8977
+ "Content-Type": "application/json"
8978
+ },
8971
8979
  body: JSON.stringify({
8972
8980
  model,
8973
8981
  // Modern OpenAI field; the portal reads only `max_completion_tokens`
@@ -9407,10 +9415,15 @@ async function synthesizeFacet(facet, ctx, options, prior) {
9407
9415
  limit,
9408
9416
  types: ["fact"],
9409
9417
  maxTokens: DEFAULT_FACET_MAX_TOKENS,
9410
- // Profile-facet synthesis is a background op, so it marks its own prompt.
9411
- // reflect() itself must NOT mark — it also serves the user's own questions,
9412
- // and that traffic is chat, not an internal flow. See ../internalFlowMarker.
9413
- systemPrompt: withInternalFlowMarker(buildFacetSystemPrompt(facet)),
9418
+ // Profile-facet synthesis is a background op, so it marks its own prompt AND
9419
+ // names its own task. reflect() itself must do neither — it also serves the
9420
+ // user's own questions, and that traffic is chat, not an internal flow. See
9421
+ // ../internalFlowMarker and ReflectOptions.taskType.
9422
+ taskType: "memory_profile_synth",
9423
+ systemPrompt: withInternalFlowMarker(FACET_SYSTEM_PROMPT),
9424
+ // The facet's label/guidance/response-fields, on the user turn — what keeps
9425
+ // the system half fixed and therefore server-ownable.
9426
+ userInstructions: buildFacetUserInstructions(facet),
9414
9427
  responseSchema: facetResponseSchema(facet.key),
9415
9428
  memories
9416
9429
  });
@@ -9468,19 +9481,22 @@ function fallbackSection(facet, prior) {
9468
9481
  stale: true
9469
9482
  };
9470
9483
  }
9471
- function buildFacetSystemPrompt(facet) {
9472
- const structuredField = FACET_STRUCTURED_RESPONSE_HINT[facet.key] ?? "";
9473
- return `You are writing the "${facet.label}" section of a person's shareable profile, using their private memories (supplied as evidence) as the only source of truth.
9474
-
9475
- Task for this section:
9476
- ${facet.guidance}
9484
+ var FACET_SYSTEM_PROMPT = `You are writing one section of a person's shareable profile, using their private memories (supplied as evidence) as the only source of truth. The user turn names the section, states the task for it, and gives the exact JSON shape to respond in.
9477
9485
 
9478
9486
  Rules:
9479
9487
  - Ground every claim in the supplied memories \u2014 never invent, infer beyond, or embellish what they support.
9480
9488
  - If the memories don't cover this section, return an empty summary with hasEvidence=false. Do not pad or guess.
9481
9489
  - Write in third person about the person, in a natural voice suitable for a public profile (no "I"/"you", no name repetition).
9482
9490
  - Be concise and specific; no preamble, hedging, or meta-commentary.
9483
- - Respond as JSON: { "summary": <the section text, or "">, "hasEvidence": <true|false>${structuredField} }.`;
9491
+ - Respond as JSON in exactly the shape the user turn states, with no extra fields.`;
9492
+ function buildFacetUserInstructions(facet) {
9493
+ const structuredField = FACET_STRUCTURED_RESPONSE_HINT[facet.key] ?? "";
9494
+ return `Section: "${facet.label}"
9495
+
9496
+ Task for this section:
9497
+ ${facet.guidance}
9498
+
9499
+ Respond as JSON: { "summary": <the section text, or "">, "hasEvidence": <true|false>${structuredField} }.`;
9484
9500
  }
9485
9501
  function extractFacetText(structured) {
9486
9502
  if (structured && typeof structured === "object") {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  recall
3
- } from "./chunk-EJ65X4GI.mjs";
3
+ } from "./chunk-6IBFFWCE.mjs";
4
4
  import "./chunk-CJ32HO6Q.mjs";
5
5
  import "./chunk-GYKJOWEO.mjs";
6
6
  export {
@@ -19193,6 +19193,9 @@ async function decomposeQuery(query, options) {
19193
19193
  ...options.baseUrl !== void 0 && { baseUrl: options.baseUrl },
19194
19194
  model: options.model ?? DEFAULT_MODEL,
19195
19195
  systemPrompt: SYSTEM_PROMPT,
19196
+ // The query itself rides the user turn below, so SYSTEM_PROMPT is the whole
19197
+ // fixed instruction — which is what lets the portal own it outright.
19198
+ taskType: "memory_decompose",
19196
19199
  // Wrap the bare query in a task framing so the model treats it as
19197
19200
  // input to classify, not as a question to answer conversationally.
19198
19201
  // Bare-query inputs caused Anthropic models to respond with prose
@@ -19307,9 +19310,9 @@ function recencyMultiplier(updatedAt, opts = {}) {
19307
19310
  const updatedMs = updatedAt.getTime();
19308
19311
  if (!Number.isFinite(updatedMs)) return noDate;
19309
19312
  const now = opts.now ?? /* @__PURE__ */ new Date();
19310
- const nowMs2 = now.getTime();
19311
- if (!Number.isFinite(nowMs2)) return noDate;
19312
- const ageDays = Math.max(0, (nowMs2 - updatedMs) / DAY_MS);
19313
+ const nowMs3 = now.getTime();
19314
+ if (!Number.isFinite(nowMs3)) return noDate;
19315
+ const ageDays = Math.max(0, (nowMs3 - updatedMs) / DAY_MS);
19313
19316
  const perYearDecay = opts.perYearDecay ?? DEFAULT_PER_YEAR_DECAY;
19314
19317
  const floor = opts.floor ?? DEFAULT_FLOOR;
19315
19318
  const decay = perYearDecay * (ageDays / 365);
@@ -19655,6 +19658,7 @@ function createVaultEmbeddingCache(maxSize = DEFAULT_VAULT_CACHE_SIZE) {
19655
19658
  }
19656
19659
 
19657
19660
  // src/lib/memoryVault/searchTool.ts
19661
+ var nowMs = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
19658
19662
  var warnedProgrammaticDecomposeIgnored = false;
19659
19663
  function normalizeEventTimeKind(kind) {
19660
19664
  return kind === "point" || kind === "range" || kind === "ongoing" ? kind : null;
@@ -19887,6 +19891,7 @@ async function rankFusedVaultMemoriesAsync(query, queryEmbedding, items, options
19887
19891
  const rerankTopN = options.rerankTopN ?? 30;
19888
19892
  const ceWeight = options.ceWeight ?? 0.1;
19889
19893
  const headSlice = v2Ranked.slice(0, rerankTopN);
19894
+ const ceStart = nowMs();
19890
19895
  try {
19891
19896
  const reranked = await rerankPairs(
19892
19897
  query,
@@ -19914,6 +19919,8 @@ async function rankFusedVaultMemoriesAsync(query, queryEmbedding, items, options
19914
19919
  getLogger().warn("[memory/search] cross-encoder rerank failed; using V2 ranking", err);
19915
19920
  }
19916
19921
  combined = v2Ranked;
19922
+ } finally {
19923
+ if (options.rerankStats) options.rerankStats.ms += nowMs() - ceStart;
19917
19924
  }
19918
19925
  } else {
19919
19926
  combined = v2Ranked;
@@ -20080,6 +20087,7 @@ async function rankComposite(originalQuery, originalQueryEmbedding, subQueries,
20080
20087
  const ceWeight = options.ceWeight ?? 0.1;
20081
20088
  const headSlice = combined.slice(0, rerankTopN);
20082
20089
  const tailSlice = combined.slice(rerankTopN);
20090
+ const ceStart = nowMs();
20083
20091
  try {
20084
20092
  const reranked = await rerankPairs(
20085
20093
  originalQuery,
@@ -20107,6 +20115,8 @@ async function rankComposite(originalQuery, originalQueryEmbedding, subQueries,
20107
20115
  err
20108
20116
  );
20109
20117
  }
20118
+ } finally {
20119
+ if (options.rerankStats) options.rerankStats.ms += nowMs() - ceStart;
20110
20120
  }
20111
20121
  }
20112
20122
  if (options?.mmr) {
@@ -20233,11 +20243,13 @@ async function buildProjectedCorpus(query, vaultCtx, embeddingOptions, cache, qu
20233
20243
  rowsDecrypted: 0
20234
20244
  };
20235
20245
  }
20246
+ const queryEmbedStart = nowMs();
20236
20247
  const queryEmbedding = await embedQueryOrDegrade(
20237
20248
  query,
20238
20249
  embeddingOptions,
20239
20250
  opts.onEmbeddingDegraded
20240
20251
  );
20252
+ if (opts.embedStats) opts.embedStats.queryMs += nowMs() - queryEmbedStart;
20241
20253
  const embeddingsDegraded = queryEmbedding.length === 0;
20242
20254
  const currentModel = embeddingOptions.model ?? DEFAULT_API_EMBEDDING_MODEL;
20243
20255
  const vectored = [];
@@ -20290,6 +20302,7 @@ async function buildProjectedCorpus(query, vaultCtx, embeddingOptions, cache, qu
20290
20302
  const laneRows = laneFetched.filter((m) => !isEncrypted(m.content));
20291
20303
  if (laneRows.length > 0) {
20292
20304
  let laneVecs;
20305
+ if (opts.embedStats) opts.embedStats.rowsEmbedded += laneRows.length;
20293
20306
  try {
20294
20307
  laneVecs = await generateEmbeddings(
20295
20308
  laneRows.map((m) => m.content),
@@ -20368,6 +20381,10 @@ async function buildProjectedCorpus(query, vaultCtx, embeddingOptions, cache, qu
20368
20381
  async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache, searchOptions) {
20369
20382
  if (!query || typeof query !== "string") {
20370
20383
  return {
20384
+ // Nothing was read and nothing was embedded — the short-circuit fires
20385
+ // before either.
20386
+ queryEmbedMs: 0,
20387
+ rowsEmbedded: 0,
20371
20388
  memories: [],
20372
20389
  embeddedItems: [],
20373
20390
  queryEmbedding: [],
@@ -20400,6 +20417,7 @@ async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache,
20400
20417
  embeddingsUnavailable = true;
20401
20418
  embeddingFailure = true;
20402
20419
  };
20420
+ const embedStats = { queryMs: 0, rowsEmbedded: 0 };
20403
20421
  if (searchOptions?.decryptLast) {
20404
20422
  const forceIncludeIds = [
20405
20423
  ...searchOptions.entityRanking ?? [],
@@ -20411,7 +20429,8 @@ async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache,
20411
20429
  admitFloor: ADMIT_FLOOR,
20412
20430
  unembeddedCap: UNEMBEDDED_CAP,
20413
20431
  ...forceIncludeIds.length > 0 && { forceIncludeIds },
20414
- onEmbeddingDegraded
20432
+ onEmbeddingDegraded,
20433
+ embedStats
20415
20434
  });
20416
20435
  if (corpus.vaultSize === 0) {
20417
20436
  return {
@@ -20422,7 +20441,9 @@ async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache,
20422
20441
  embeddingsUnavailable: false,
20423
20442
  embeddingFailure: false,
20424
20443
  decryptLast: true,
20425
- rowsDecrypted: corpus.rowsDecrypted
20444
+ rowsDecrypted: corpus.rowsDecrypted,
20445
+ queryEmbedMs: embedStats.queryMs,
20446
+ rowsEmbedded: embedStats.rowsEmbedded
20426
20447
  };
20427
20448
  }
20428
20449
  ({ memories, embeddedItems, queryEmbedding, vaultSize } = corpus);
@@ -20439,7 +20460,9 @@ async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache,
20439
20460
  decryptLast: true,
20440
20461
  // Rows WERE decrypt-attempted even though none came back readable — that
20441
20462
  // is the whole point of reporting attempts rather than successes.
20442
- rowsDecrypted: corpus.rowsDecrypted
20463
+ rowsDecrypted: corpus.rowsDecrypted,
20464
+ queryEmbedMs: embedStats.queryMs,
20465
+ rowsEmbedded: embedStats.rowsEmbedded
20443
20466
  };
20444
20467
  }
20445
20468
  } else {
@@ -20464,10 +20487,14 @@ async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache,
20464
20487
  embeddingsUnavailable: false,
20465
20488
  embeddingFailure: false,
20466
20489
  decryptLast: false,
20467
- rowsDecrypted: loaded.length
20490
+ rowsDecrypted: loaded.length,
20491
+ queryEmbedMs: embedStats.queryMs,
20492
+ rowsEmbedded: embedStats.rowsEmbedded
20468
20493
  };
20469
20494
  }
20495
+ const legacyEmbedStart = nowMs();
20470
20496
  queryEmbedding = await embedQueryOrDegrade(query, embeddingOptions, onEmbeddingDegraded);
20497
+ embedStats.queryMs += nowMs() - legacyEmbedStart;
20471
20498
  const currentModel = embeddingOptions.model ?? DEFAULT_API_EMBEDDING_MODEL;
20472
20499
  const uncachedTexts = [];
20473
20500
  const uncachedIndices = [];
@@ -20501,6 +20528,7 @@ async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache,
20501
20528
  }
20502
20529
  if (uncachedTexts.length > 0) {
20503
20530
  let newEmbeddings;
20531
+ embedStats.rowsEmbedded += uncachedTexts.length;
20504
20532
  try {
20505
20533
  newEmbeddings = await generateEmbeddings(uncachedTexts, embeddingOptions);
20506
20534
  } catch (err) {
@@ -20544,6 +20572,8 @@ async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache,
20544
20572
  embeddingsUnavailable = true;
20545
20573
  }
20546
20574
  return {
20575
+ queryEmbedMs: embedStats.queryMs,
20576
+ rowsEmbedded: embedStats.rowsEmbedded,
20547
20577
  memories,
20548
20578
  embeddedItems,
20549
20579
  queryEmbedding,
@@ -20594,11 +20624,15 @@ async function rankPreparedVaultCandidates(query, prepared, embeddingOptions, se
20594
20624
  results: results2,
20595
20625
  vaultSize: out.vaultSize,
20596
20626
  reranked: out.reranked ?? false,
20627
+ // Read straight off the shared stats object rather than being passed in at
20628
+ // each call site: every return in this function funnels through here, so a
20629
+ // path added later reports the CE bill without having to remember to.
20630
+ rerankMs: rerankStats.ms,
20597
20631
  hadV2Head: out.hadV2Head ?? false,
20598
20632
  embeddingsUnavailable
20599
20633
  };
20600
20634
  };
20601
- const rerankStats = { applied: false };
20635
+ const rerankStats = { applied: false, ms: 0 };
20602
20636
  const useFusion = searchOptions?.useFusion ?? true;
20603
20637
  const tuning = {
20604
20638
  ...searchOptions?.recencyAlpha !== void 0 && { recencyAlpha: searchOptions.recencyAlpha },
@@ -20717,13 +20751,16 @@ async function searchVaultMemoriesWithSize(query, vaultCtx, embeddingOptions, ca
20717
20751
  results: [],
20718
20752
  vaultSize: 0,
20719
20753
  reranked: false,
20754
+ rerankMs: 0,
20720
20755
  hadV2Head: false,
20721
20756
  embeddingsUnavailable: false,
20722
20757
  rankedOnCosine: false,
20723
20758
  // No storage read happened, so nothing was decrypted; report the path the
20724
20759
  // caller asked for so this turn is still attributable.
20725
20760
  decryptLast: !!searchOptions?.decryptLast,
20726
- rowsDecrypted: 0
20761
+ rowsDecrypted: 0,
20762
+ queryEmbedMs: 0,
20763
+ rowsEmbedded: 0
20727
20764
  };
20728
20765
  }
20729
20766
  if (searchOptions?.decompose === "llm" && normalizeSubQueries(searchOptions.subQueries).length < 2 && !warnedProgrammaticDecomposeIgnored) {
@@ -20744,11 +20781,14 @@ async function searchVaultMemoriesWithSize(query, vaultCtx, embeddingOptions, ca
20744
20781
  results: [],
20745
20782
  vaultSize: 0,
20746
20783
  reranked: false,
20784
+ rerankMs: 0,
20747
20785
  hadV2Head: false,
20748
20786
  embeddingsUnavailable: prepared.embeddingsUnavailable,
20749
20787
  rankedOnCosine: false,
20750
20788
  decryptLast: prepared.decryptLast,
20751
- rowsDecrypted: prepared.rowsDecrypted
20789
+ rowsDecrypted: prepared.rowsDecrypted,
20790
+ queryEmbedMs: prepared.queryEmbedMs,
20791
+ rowsEmbedded: prepared.rowsEmbedded
20752
20792
  };
20753
20793
  }
20754
20794
  if (prepared.memories.length === 0) {
@@ -20756,11 +20796,14 @@ async function searchVaultMemoriesWithSize(query, vaultCtx, embeddingOptions, ca
20756
20796
  results: [],
20757
20797
  vaultSize: prepared.vaultSize,
20758
20798
  reranked: false,
20799
+ rerankMs: 0,
20759
20800
  hadV2Head: false,
20760
20801
  embeddingsUnavailable: prepared.embeddingsUnavailable,
20761
20802
  rankedOnCosine: false,
20762
20803
  decryptLast: prepared.decryptLast,
20763
- rowsDecrypted: prepared.rowsDecrypted
20804
+ rowsDecrypted: prepared.rowsDecrypted,
20805
+ queryEmbedMs: prepared.queryEmbedMs,
20806
+ rowsEmbedded: prepared.rowsEmbedded
20764
20807
  };
20765
20808
  }
20766
20809
  const ranked = await rankPreparedVaultCandidates(
@@ -20773,7 +20816,9 @@ async function searchVaultMemoriesWithSize(query, vaultCtx, embeddingOptions, ca
20773
20816
  ...ranked,
20774
20817
  rankedOnCosine: !ranked.embeddingsUnavailable,
20775
20818
  decryptLast: prepared.decryptLast,
20776
- rowsDecrypted: prepared.rowsDecrypted
20819
+ rowsDecrypted: prepared.rowsDecrypted,
20820
+ queryEmbedMs: prepared.queryEmbedMs,
20821
+ rowsEmbedded: prepared.rowsEmbedded
20777
20822
  };
20778
20823
  }
20779
20824
  async function searchVaultMemories(query, vaultCtx, embeddingOptions, cache, searchOptions) {
@@ -20878,7 +20923,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
20878
20923
  subQueries = decomp.subQueries;
20879
20924
  }
20880
20925
  }
20881
- const { recall: recall2 } = await import("./recall-T7CNXTQ3.mjs");
20926
+ const { recall: recall2 } = await import("./recall-HOVWMT4Q.mjs");
20882
20927
  let recallDegraded = [];
20883
20928
  const result = await recall2(
20884
20929
  query,
@@ -21131,29 +21176,29 @@ async function traverseGraphLane(query, entityCtx, options = {}) {
21131
21176
  var DEFAULT_REFINER_MODEL = "inclusionai/ling-2.6-flash";
21132
21177
  var DEFAULT_REFINER_ATTEMPTS = 1;
21133
21178
  var DEFAULT_REFINER_TOTAL_TIMEOUT_MS = 8e3;
21179
+ var REFINER_SYSTEM_PROMPT = `You help a memory-retrieval system decide which related topics to explore.
21180
+
21181
+ Given a user's question and a numbered list of candidate topics/entities linked to memories found so far, pick the ones most likely to lead to memories that help ANSWER the question. Never return more than the maximum the user turn asks for. Prefer topics semantically related to the question; ignore incidental ones.
21182
+
21183
+ Output strict JSON, no prose: { "expand": [<entity names to expand, verbatim from the list>] }`;
21134
21184
  function createLlmNeighborRefiner(options) {
21135
21185
  return {
21136
21186
  async refine(query, candidates, limit) {
21137
21187
  if (candidates.length === 0) return [];
21138
21188
  const numbered = candidates.map((name, i) => `[${i + 1}] ${name}`).join("\n");
21139
- const systemPrompt = `You help a memory-retrieval system decide which related topics to explore.
21140
-
21141
- Given a user's question and a numbered list of candidate topics/entities linked to memories found so far, pick the ones most likely to lead to memories that help ANSWER the question. Choose at most ${limit}. Prefer topics semantically related to the question; ignore incidental ones.
21142
-
21143
- Output strict JSON, no prose: { "expand": [<entity names to expand, verbatim from the list>] }`;
21144
21189
  const parsed = await callPortalJsonCompletion({
21145
21190
  ...options.apiKey !== void 0 && { apiKey: options.apiKey },
21146
21191
  ...options.getToken !== void 0 && { getToken: options.getToken },
21147
21192
  ...options.baseUrl !== void 0 && { baseUrl: options.baseUrl },
21148
21193
  taskType: "memory_graph",
21149
21194
  model: options.model ?? DEFAULT_REFINER_MODEL,
21150
- systemPrompt,
21195
+ systemPrompt: REFINER_SYSTEM_PROMPT,
21151
21196
  userMessage: `Question: ${query}
21152
21197
 
21153
21198
  Candidate topics:
21154
21199
  ${numbered}
21155
21200
 
21156
- Which should be expanded?`,
21201
+ Which should be expanded? Choose at most ${limit}.`,
21157
21202
  tag: "memory/graph-refine",
21158
21203
  maxAttempts: options.maxAttempts ?? DEFAULT_REFINER_ATTEMPTS,
21159
21204
  totalTimeoutMs: options.totalTimeoutMs ?? DEFAULT_REFINER_TOTAL_TIMEOUT_MS,
@@ -21418,7 +21463,7 @@ function scoreEventTimeOverlap(memoryStart, memoryEnd, memoryKind, window2) {
21418
21463
  var DEFAULT_LIMIT = 8;
21419
21464
  var DEFAULT_BUDGET = "low";
21420
21465
  var DEFAULT_FACT_MIN_SCORE = 0.1;
21421
- var nowMs = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
21466
+ var nowMs2 = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
21422
21467
  var DEFAULT_CHUNK_MIN_SCORE = 0.5;
21423
21468
  function flagsForBudget(budget) {
21424
21469
  switch (budget) {
@@ -21451,7 +21496,7 @@ async function recall(query, ctx, options = {}) {
21451
21496
  const flags = flagsForBudget(usedBudget);
21452
21497
  const normalizedFacets = normalizeSubQueries(options.subQueries);
21453
21498
  const subQueries = normalizedFacets.length >= 2 ? normalizedFacets : void 0;
21454
- const t0 = nowMs();
21499
+ const t0 = nowMs2();
21455
21500
  let prepMs = 0;
21456
21501
  let factLaneMs = 0;
21457
21502
  let chunkLaneMs = 0;
@@ -21460,6 +21505,9 @@ async function recall(query, ctx, options = {}) {
21460
21505
  const chunkResults = [];
21461
21506
  let vaultSize;
21462
21507
  let didRerank = false;
21508
+ let rerankMs = 0;
21509
+ let queryEmbedMs = 0;
21510
+ let vaultRowsEmbedded;
21463
21511
  let hadV2Head = false;
21464
21512
  let embeddingsUnavailable = false;
21465
21513
  let chunkEmbedFailed = false;
@@ -21484,12 +21532,15 @@ async function recall(query, ctx, options = {}) {
21484
21532
  ...vaultSize !== void 0 && { vaultSize },
21485
21533
  ...decryptLastRan !== void 0 && { decryptLast: decryptLastRan },
21486
21534
  ...vaultRowsDecrypted !== void 0 && { vaultRowsDecrypted },
21535
+ ...vaultRowsEmbedded !== void 0 && { vaultRowsEmbedded },
21487
21536
  factCount: factResults.length,
21488
21537
  chunkCount: chunkResults.length,
21489
21538
  timings: {
21490
- total: nowMs() - t0,
21539
+ total: nowMs2() - t0,
21491
21540
  prep: prepMs,
21492
21541
  factLane: factLaneMs,
21542
+ rerank: rerankMs,
21543
+ queryEmbed: queryEmbedMs,
21493
21544
  chunkLane: chunkLaneMs,
21494
21545
  fuse: fuseMs
21495
21546
  },
@@ -21507,7 +21558,7 @@ async function recall(query, ctx, options = {}) {
21507
21558
  const needsChunkEmbedding = types.includes("chunk") && ctx.storageCtx;
21508
21559
  const wantsTemporal = types.includes("fact") && ctx.vaultCtx;
21509
21560
  const graphRefiner = flags.traverse && options.graphRefine && options.decomposeOptions ? createLlmNeighborRefiner(options.decomposeOptions) : void 0;
21510
- const prepStart = nowMs();
21561
+ const prepStart = nowMs2();
21511
21562
  const [queryEmbedding, entityRanking, temporalRanking] = await Promise.all([
21512
21563
  // The chunk lane is cosine-only — `searchChunksOp` needs a real vector, and
21513
21564
  // there is no lexical fallback for it — so an embeddings outage must SKIP the
@@ -21545,14 +21596,17 @@ async function recall(query, ctx, options = {}) {
21545
21596
  ),
21546
21597
  wantsTemporal ? safeLane("temporal", () => buildTemporalLaneRanking(query, ctx.vaultCtx, options.now)) : Promise.resolve([])
21547
21598
  ]);
21548
- prepMs = nowMs() - prepStart;
21599
+ prepMs = nowMs2() - prepStart;
21549
21600
  if (types.includes("fact") && ctx.vaultCtx && ctx.vaultCache) {
21550
- const factStart = nowMs();
21601
+ const factStart = nowMs2();
21551
21602
  const vaultMinScore = options.minScore ?? DEFAULT_FACT_MIN_SCORE;
21552
21603
  const {
21553
21604
  results,
21554
21605
  vaultSize: size,
21555
21606
  reranked,
21607
+ rerankMs: factRerankMs,
21608
+ queryEmbedMs: factQueryEmbedMs,
21609
+ rowsEmbedded: factRowsEmbedded,
21556
21610
  hadV2Head: v2Head,
21557
21611
  embeddingsUnavailable: factEmbeddingsUnavailable,
21558
21612
  rankedOnCosine: factRankedOnCosine,
@@ -21611,16 +21665,19 @@ async function recall(query, ctx, options = {}) {
21611
21665
  );
21612
21666
  vaultSize = size;
21613
21667
  didRerank = reranked;
21668
+ rerankMs = factRerankMs;
21669
+ queryEmbedMs = factQueryEmbedMs;
21670
+ vaultRowsEmbedded = factRowsEmbedded;
21614
21671
  hadV2Head = v2Head;
21615
21672
  if (factEmbeddingsUnavailable) embeddingsUnavailable = true;
21616
21673
  factLaneRankedOnCosine = factRankedOnCosine;
21617
21674
  decryptLastRan = factDecryptLast;
21618
21675
  vaultRowsDecrypted = factRowsDecrypted;
21619
- factLaneMs = nowMs() - factStart;
21676
+ factLaneMs = nowMs2() - factStart;
21620
21677
  }
21621
21678
  if (chunkEmbedFailed && !factLaneRankedOnCosine) embeddingsUnavailable = true;
21622
21679
  if (types.includes("chunk") && ctx.storageCtx && queryEmbedding) {
21623
- const chunkStart = nowMs();
21680
+ const chunkStart = nowMs2();
21624
21681
  const chunkMinScore = options.minScore ?? DEFAULT_CHUNK_MIN_SCORE;
21625
21682
  const results = await searchChunksOp(ctx.storageCtx, queryEmbedding, {
21626
21683
  limit: types.includes("fact") ? Math.max(limit * 2, 16) : limit,
@@ -21637,9 +21694,9 @@ async function recall(query, ctx, options = {}) {
21637
21694
  (r) => r.chunkText.trim()
21638
21695
  )
21639
21696
  );
21640
- chunkLaneMs = nowMs() - chunkStart;
21697
+ chunkLaneMs = nowMs2() - chunkStart;
21641
21698
  }
21642
- const fuseStart = nowMs();
21699
+ const fuseStart = nowMs2();
21643
21700
  if (types.length === 1 || factResults.length === 0 || chunkResults.length === 0) {
21644
21701
  const memories2 = [
21645
21702
  ...factResults.map((r) => toFactMemory(r, options.now)),
@@ -21647,7 +21704,7 @@ async function recall(query, ctx, options = {}) {
21647
21704
  ];
21648
21705
  memories2.sort((a, b) => b.score - a.score);
21649
21706
  const candidateCount = factResults.length + chunkResults.length;
21650
- fuseMs = nowMs() - fuseStart;
21707
+ fuseMs = nowMs2() - fuseStart;
21651
21708
  emitDiagnostics(candidateCount);
21652
21709
  return {
21653
21710
  memories: memories2.slice(0, limit),
@@ -21702,7 +21759,7 @@ async function recall(query, ctx, options = {}) {
21702
21759
  suppressed = next;
21703
21760
  memories = selectWith(suppressed);
21704
21761
  }
21705
- fuseMs = nowMs() - fuseStart;
21762
+ fuseMs = nowMs2() - fuseStart;
21706
21763
  emitDiagnostics(byId.size);
21707
21764
  return {
21708
21765
  memories,