@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.
@@ -20236,9 +20236,9 @@ function recencyMultiplier(updatedAt, opts = {}) {
20236
20236
  const updatedMs = updatedAt.getTime();
20237
20237
  if (!Number.isFinite(updatedMs)) return noDate;
20238
20238
  const now = opts.now ?? /* @__PURE__ */ new Date();
20239
- const nowMs2 = now.getTime();
20240
- if (!Number.isFinite(nowMs2)) return noDate;
20241
- const ageDays2 = Math.max(0, (nowMs2 - updatedMs) / DAY_MS2);
20239
+ const nowMs3 = now.getTime();
20240
+ if (!Number.isFinite(nowMs3)) return noDate;
20241
+ const ageDays2 = Math.max(0, (nowMs3 - updatedMs) / DAY_MS2);
20242
20242
  const perYearDecay = opts.perYearDecay ?? DEFAULT_PER_YEAR_DECAY;
20243
20243
  const floor = opts.floor ?? DEFAULT_FLOOR;
20244
20244
  const decay = perYearDecay * (ageDays2 / 365);
@@ -20915,6 +20915,9 @@ async function decomposeQuery(query, options) {
20915
20915
  ...options.baseUrl !== void 0 && { baseUrl: options.baseUrl },
20916
20916
  model: options.model ?? DEFAULT_MODEL,
20917
20917
  systemPrompt: SYSTEM_PROMPT,
20918
+ // The query itself rides the user turn below, so SYSTEM_PROMPT is the whole
20919
+ // fixed instruction — which is what lets the portal own it outright.
20920
+ taskType: "memory_decompose",
20918
20921
  // Wrap the bare query in a task framing so the model treats it as
20919
20922
  // input to classify, not as a question to answer conversationally.
20920
20923
  // Bare-query inputs caused Anthropic models to respond with prose
@@ -21220,24 +21223,19 @@ function createLlmNeighborRefiner(options) {
21220
21223
  async refine(query, candidates, limit) {
21221
21224
  if (candidates.length === 0) return [];
21222
21225
  const numbered = candidates.map((name, i) => `[${i + 1}] ${name}`).join("\n");
21223
- const systemPrompt = `You help a memory-retrieval system decide which related topics to explore.
21224
-
21225
- 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.
21226
-
21227
- Output strict JSON, no prose: { "expand": [<entity names to expand, verbatim from the list>] }`;
21228
21226
  const parsed = await callPortalJsonCompletion({
21229
21227
  ...options.apiKey !== void 0 && { apiKey: options.apiKey },
21230
21228
  ...options.getToken !== void 0 && { getToken: options.getToken },
21231
21229
  ...options.baseUrl !== void 0 && { baseUrl: options.baseUrl },
21232
21230
  taskType: "memory_graph",
21233
21231
  model: options.model ?? DEFAULT_REFINER_MODEL,
21234
- systemPrompt,
21232
+ systemPrompt: REFINER_SYSTEM_PROMPT,
21235
21233
  userMessage: `Question: ${query}
21236
21234
 
21237
21235
  Candidate topics:
21238
21236
  ${numbered}
21239
21237
 
21240
- Which should be expanded?`,
21238
+ Which should be expanded? Choose at most ${limit}.`,
21241
21239
  tag: "memory/graph-refine",
21242
21240
  maxAttempts: options.maxAttempts ?? DEFAULT_REFINER_ATTEMPTS,
21243
21241
  totalTimeoutMs: options.totalTimeoutMs ?? DEFAULT_REFINER_TOTAL_TIMEOUT_MS,
@@ -21258,7 +21256,7 @@ Which should be expanded?`,
21258
21256
  }
21259
21257
  };
21260
21258
  }
21261
- var MAX_HOPS, ENTITY_FANOUT, MIN_REFINER_CANDIDATES, MAX_REFINER_CANDIDATES, NODE_BUDGET, VAULT_SIZE_HOP_CAP, DEFAULT_REFINER_MODEL, DEFAULT_REFINER_ATTEMPTS, DEFAULT_REFINER_TOTAL_TIMEOUT_MS;
21259
+ var MAX_HOPS, ENTITY_FANOUT, MIN_REFINER_CANDIDATES, MAX_REFINER_CANDIDATES, NODE_BUDGET, VAULT_SIZE_HOP_CAP, DEFAULT_REFINER_MODEL, DEFAULT_REFINER_ATTEMPTS, DEFAULT_REFINER_TOTAL_TIMEOUT_MS, REFINER_SYSTEM_PROMPT;
21262
21260
  var init_graphTraversal = __esm({
21263
21261
  "src/lib/memory/graphTraversal.ts"() {
21264
21262
  "use strict";
@@ -21277,6 +21275,11 @@ var init_graphTraversal = __esm({
21277
21275
  DEFAULT_REFINER_MODEL = "inclusionai/ling-2.6-flash";
21278
21276
  DEFAULT_REFINER_ATTEMPTS = 1;
21279
21277
  DEFAULT_REFINER_TOTAL_TIMEOUT_MS = 8e3;
21278
+ REFINER_SYSTEM_PROMPT = `You help a memory-retrieval system decide which related topics to explore.
21279
+
21280
+ 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.
21281
+
21282
+ Output strict JSON, no prose: { "expand": [<entity names to expand, verbatim from the list>] }`;
21280
21283
  }
21281
21284
  });
21282
21285
 
@@ -21576,6 +21579,9 @@ async function recall(query, ctx, options = {}) {
21576
21579
  const chunkResults = [];
21577
21580
  let vaultSize;
21578
21581
  let didRerank = false;
21582
+ let rerankMs = 0;
21583
+ let queryEmbedMs = 0;
21584
+ let vaultRowsEmbedded;
21579
21585
  let hadV2Head = false;
21580
21586
  let embeddingsUnavailable = false;
21581
21587
  let chunkEmbedFailed = false;
@@ -21600,12 +21606,15 @@ async function recall(query, ctx, options = {}) {
21600
21606
  ...vaultSize !== void 0 && { vaultSize },
21601
21607
  ...decryptLastRan !== void 0 && { decryptLast: decryptLastRan },
21602
21608
  ...vaultRowsDecrypted !== void 0 && { vaultRowsDecrypted },
21609
+ ...vaultRowsEmbedded !== void 0 && { vaultRowsEmbedded },
21603
21610
  factCount: factResults.length,
21604
21611
  chunkCount: chunkResults.length,
21605
21612
  timings: {
21606
21613
  total: nowMs() - t0,
21607
21614
  prep: prepMs,
21608
21615
  factLane: factLaneMs,
21616
+ rerank: rerankMs,
21617
+ queryEmbed: queryEmbedMs,
21609
21618
  chunkLane: chunkLaneMs,
21610
21619
  fuse: fuseMs
21611
21620
  },
@@ -21669,6 +21678,9 @@ async function recall(query, ctx, options = {}) {
21669
21678
  results,
21670
21679
  vaultSize: size,
21671
21680
  reranked,
21681
+ rerankMs: factRerankMs,
21682
+ queryEmbedMs: factQueryEmbedMs,
21683
+ rowsEmbedded: factRowsEmbedded,
21672
21684
  hadV2Head: v2Head,
21673
21685
  embeddingsUnavailable: factEmbeddingsUnavailable,
21674
21686
  rankedOnCosine: factRankedOnCosine,
@@ -21727,6 +21739,9 @@ async function recall(query, ctx, options = {}) {
21727
21739
  );
21728
21740
  vaultSize = size;
21729
21741
  didRerank = reranked;
21742
+ rerankMs = factRerankMs;
21743
+ queryEmbedMs = factQueryEmbedMs;
21744
+ vaultRowsEmbedded = factRowsEmbedded;
21730
21745
  hadV2Head = v2Head;
21731
21746
  if (factEmbeddingsUnavailable) embeddingsUnavailable = true;
21732
21747
  factLaneRankedOnCosine = factRankedOnCosine;
@@ -22191,6 +22206,7 @@ async function rankFusedVaultMemoriesAsync(query, queryEmbedding, items, options
22191
22206
  const rerankTopN = options.rerankTopN ?? 30;
22192
22207
  const ceWeight = options.ceWeight ?? 0.1;
22193
22208
  const headSlice = v2Ranked.slice(0, rerankTopN);
22209
+ const ceStart = nowMs2();
22194
22210
  try {
22195
22211
  const reranked = await rerankPairs(
22196
22212
  query,
@@ -22218,6 +22234,8 @@ async function rankFusedVaultMemoriesAsync(query, queryEmbedding, items, options
22218
22234
  getLogger().warn("[memory/search] cross-encoder rerank failed; using V2 ranking", err);
22219
22235
  }
22220
22236
  combined = v2Ranked;
22237
+ } finally {
22238
+ if (options.rerankStats) options.rerankStats.ms += nowMs2() - ceStart;
22221
22239
  }
22222
22240
  } else {
22223
22241
  combined = v2Ranked;
@@ -22384,6 +22402,7 @@ async function rankComposite(originalQuery, originalQueryEmbedding, subQueries,
22384
22402
  const ceWeight = options.ceWeight ?? 0.1;
22385
22403
  const headSlice = combined.slice(0, rerankTopN);
22386
22404
  const tailSlice = combined.slice(rerankTopN);
22405
+ const ceStart = nowMs2();
22387
22406
  try {
22388
22407
  const reranked = await rerankPairs(
22389
22408
  originalQuery,
@@ -22411,6 +22430,8 @@ async function rankComposite(originalQuery, originalQueryEmbedding, subQueries,
22411
22430
  err
22412
22431
  );
22413
22432
  }
22433
+ } finally {
22434
+ if (options.rerankStats) options.rerankStats.ms += nowMs2() - ceStart;
22414
22435
  }
22415
22436
  }
22416
22437
  if (options?.mmr) {
@@ -22537,11 +22558,13 @@ async function buildProjectedCorpus(query, vaultCtx, embeddingOptions, cache, qu
22537
22558
  rowsDecrypted: 0
22538
22559
  };
22539
22560
  }
22561
+ const queryEmbedStart = nowMs2();
22540
22562
  const queryEmbedding = await embedQueryOrDegrade(
22541
22563
  query,
22542
22564
  embeddingOptions,
22543
22565
  opts.onEmbeddingDegraded
22544
22566
  );
22567
+ if (opts.embedStats) opts.embedStats.queryMs += nowMs2() - queryEmbedStart;
22545
22568
  const embeddingsDegraded = queryEmbedding.length === 0;
22546
22569
  const currentModel = embeddingOptions.model ?? DEFAULT_API_EMBEDDING_MODEL;
22547
22570
  const vectored = [];
@@ -22594,6 +22617,7 @@ async function buildProjectedCorpus(query, vaultCtx, embeddingOptions, cache, qu
22594
22617
  const laneRows = laneFetched.filter((m) => !isEncrypted(m.content));
22595
22618
  if (laneRows.length > 0) {
22596
22619
  let laneVecs;
22620
+ if (opts.embedStats) opts.embedStats.rowsEmbedded += laneRows.length;
22597
22621
  try {
22598
22622
  laneVecs = await generateEmbeddings(
22599
22623
  laneRows.map((m) => m.content),
@@ -22672,6 +22696,10 @@ async function buildProjectedCorpus(query, vaultCtx, embeddingOptions, cache, qu
22672
22696
  async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache, searchOptions) {
22673
22697
  if (!query || typeof query !== "string") {
22674
22698
  return {
22699
+ // Nothing was read and nothing was embedded — the short-circuit fires
22700
+ // before either.
22701
+ queryEmbedMs: 0,
22702
+ rowsEmbedded: 0,
22675
22703
  memories: [],
22676
22704
  embeddedItems: [],
22677
22705
  queryEmbedding: [],
@@ -22704,6 +22732,7 @@ async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache,
22704
22732
  embeddingsUnavailable = true;
22705
22733
  embeddingFailure = true;
22706
22734
  };
22735
+ const embedStats = { queryMs: 0, rowsEmbedded: 0 };
22707
22736
  if (searchOptions?.decryptLast) {
22708
22737
  const forceIncludeIds = [
22709
22738
  ...searchOptions.entityRanking ?? [],
@@ -22715,7 +22744,8 @@ async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache,
22715
22744
  admitFloor: ADMIT_FLOOR,
22716
22745
  unembeddedCap: UNEMBEDDED_CAP,
22717
22746
  ...forceIncludeIds.length > 0 && { forceIncludeIds },
22718
- onEmbeddingDegraded
22747
+ onEmbeddingDegraded,
22748
+ embedStats
22719
22749
  });
22720
22750
  if (corpus.vaultSize === 0) {
22721
22751
  return {
@@ -22726,7 +22756,9 @@ async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache,
22726
22756
  embeddingsUnavailable: false,
22727
22757
  embeddingFailure: false,
22728
22758
  decryptLast: true,
22729
- rowsDecrypted: corpus.rowsDecrypted
22759
+ rowsDecrypted: corpus.rowsDecrypted,
22760
+ queryEmbedMs: embedStats.queryMs,
22761
+ rowsEmbedded: embedStats.rowsEmbedded
22730
22762
  };
22731
22763
  }
22732
22764
  ({ memories, embeddedItems, queryEmbedding, vaultSize } = corpus);
@@ -22743,7 +22775,9 @@ async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache,
22743
22775
  decryptLast: true,
22744
22776
  // Rows WERE decrypt-attempted even though none came back readable — that
22745
22777
  // is the whole point of reporting attempts rather than successes.
22746
- rowsDecrypted: corpus.rowsDecrypted
22778
+ rowsDecrypted: corpus.rowsDecrypted,
22779
+ queryEmbedMs: embedStats.queryMs,
22780
+ rowsEmbedded: embedStats.rowsEmbedded
22747
22781
  };
22748
22782
  }
22749
22783
  } else {
@@ -22768,10 +22802,14 @@ async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache,
22768
22802
  embeddingsUnavailable: false,
22769
22803
  embeddingFailure: false,
22770
22804
  decryptLast: false,
22771
- rowsDecrypted: loaded.length
22805
+ rowsDecrypted: loaded.length,
22806
+ queryEmbedMs: embedStats.queryMs,
22807
+ rowsEmbedded: embedStats.rowsEmbedded
22772
22808
  };
22773
22809
  }
22810
+ const legacyEmbedStart = nowMs2();
22774
22811
  queryEmbedding = await embedQueryOrDegrade(query, embeddingOptions, onEmbeddingDegraded);
22812
+ embedStats.queryMs += nowMs2() - legacyEmbedStart;
22775
22813
  const currentModel = embeddingOptions.model ?? DEFAULT_API_EMBEDDING_MODEL;
22776
22814
  const uncachedTexts = [];
22777
22815
  const uncachedIndices = [];
@@ -22805,6 +22843,7 @@ async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache,
22805
22843
  }
22806
22844
  if (uncachedTexts.length > 0) {
22807
22845
  let newEmbeddings;
22846
+ embedStats.rowsEmbedded += uncachedTexts.length;
22808
22847
  try {
22809
22848
  newEmbeddings = await generateEmbeddings(uncachedTexts, embeddingOptions);
22810
22849
  } catch (err) {
@@ -22848,6 +22887,8 @@ async function prepareVaultCandidates(query, vaultCtx, embeddingOptions, cache,
22848
22887
  embeddingsUnavailable = true;
22849
22888
  }
22850
22889
  return {
22890
+ queryEmbedMs: embedStats.queryMs,
22891
+ rowsEmbedded: embedStats.rowsEmbedded,
22851
22892
  memories,
22852
22893
  embeddedItems,
22853
22894
  queryEmbedding,
@@ -22898,11 +22939,15 @@ async function rankPreparedVaultCandidates(query, prepared, embeddingOptions, se
22898
22939
  results: results2,
22899
22940
  vaultSize: out.vaultSize,
22900
22941
  reranked: out.reranked ?? false,
22942
+ // Read straight off the shared stats object rather than being passed in at
22943
+ // each call site: every return in this function funnels through here, so a
22944
+ // path added later reports the CE bill without having to remember to.
22945
+ rerankMs: rerankStats.ms,
22901
22946
  hadV2Head: out.hadV2Head ?? false,
22902
22947
  embeddingsUnavailable
22903
22948
  };
22904
22949
  };
22905
- const rerankStats = { applied: false };
22950
+ const rerankStats = { applied: false, ms: 0 };
22906
22951
  const useFusion = searchOptions?.useFusion ?? true;
22907
22952
  const tuning = {
22908
22953
  ...searchOptions?.recencyAlpha !== void 0 && { recencyAlpha: searchOptions.recencyAlpha },
@@ -23021,13 +23066,16 @@ async function searchVaultMemoriesWithSize(query, vaultCtx, embeddingOptions, ca
23021
23066
  results: [],
23022
23067
  vaultSize: 0,
23023
23068
  reranked: false,
23069
+ rerankMs: 0,
23024
23070
  hadV2Head: false,
23025
23071
  embeddingsUnavailable: false,
23026
23072
  rankedOnCosine: false,
23027
23073
  // No storage read happened, so nothing was decrypted; report the path the
23028
23074
  // caller asked for so this turn is still attributable.
23029
23075
  decryptLast: !!searchOptions?.decryptLast,
23030
- rowsDecrypted: 0
23076
+ rowsDecrypted: 0,
23077
+ queryEmbedMs: 0,
23078
+ rowsEmbedded: 0
23031
23079
  };
23032
23080
  }
23033
23081
  if (searchOptions?.decompose === "llm" && normalizeSubQueries(searchOptions.subQueries).length < 2 && !warnedProgrammaticDecomposeIgnored) {
@@ -23048,11 +23096,14 @@ async function searchVaultMemoriesWithSize(query, vaultCtx, embeddingOptions, ca
23048
23096
  results: [],
23049
23097
  vaultSize: 0,
23050
23098
  reranked: false,
23099
+ rerankMs: 0,
23051
23100
  hadV2Head: false,
23052
23101
  embeddingsUnavailable: prepared.embeddingsUnavailable,
23053
23102
  rankedOnCosine: false,
23054
23103
  decryptLast: prepared.decryptLast,
23055
- rowsDecrypted: prepared.rowsDecrypted
23104
+ rowsDecrypted: prepared.rowsDecrypted,
23105
+ queryEmbedMs: prepared.queryEmbedMs,
23106
+ rowsEmbedded: prepared.rowsEmbedded
23056
23107
  };
23057
23108
  }
23058
23109
  if (prepared.memories.length === 0) {
@@ -23060,11 +23111,14 @@ async function searchVaultMemoriesWithSize(query, vaultCtx, embeddingOptions, ca
23060
23111
  results: [],
23061
23112
  vaultSize: prepared.vaultSize,
23062
23113
  reranked: false,
23114
+ rerankMs: 0,
23063
23115
  hadV2Head: false,
23064
23116
  embeddingsUnavailable: prepared.embeddingsUnavailable,
23065
23117
  rankedOnCosine: false,
23066
23118
  decryptLast: prepared.decryptLast,
23067
- rowsDecrypted: prepared.rowsDecrypted
23119
+ rowsDecrypted: prepared.rowsDecrypted,
23120
+ queryEmbedMs: prepared.queryEmbedMs,
23121
+ rowsEmbedded: prepared.rowsEmbedded
23068
23122
  };
23069
23123
  }
23070
23124
  const ranked = await rankPreparedVaultCandidates(
@@ -23077,7 +23131,9 @@ async function searchVaultMemoriesWithSize(query, vaultCtx, embeddingOptions, ca
23077
23131
  ...ranked,
23078
23132
  rankedOnCosine: !ranked.embeddingsUnavailable,
23079
23133
  decryptLast: prepared.decryptLast,
23080
- rowsDecrypted: prepared.rowsDecrypted
23134
+ rowsDecrypted: prepared.rowsDecrypted,
23135
+ queryEmbedMs: prepared.queryEmbedMs,
23136
+ rowsEmbedded: prepared.rowsEmbedded
23081
23137
  };
23082
23138
  }
23083
23139
  async function searchVaultMemories(query, vaultCtx, embeddingOptions, cache, searchOptions) {
@@ -23227,7 +23283,7 @@ ${formatted}`;
23227
23283
  }
23228
23284
  };
23229
23285
  }
23230
- var warnedProgrammaticDecomposeIgnored, SUPERSESSION_SIMILARITY_THRESHOLD, SUPERSESSION_MIN_AGE_GAP_MS, SUPERSESSION_BOOST_FACTOR, SUPERSESSION_MAX_WINDOW, EMBEDDINGS_DEGRADED_EMPTY_NO_LEXICAL;
23286
+ var nowMs2, warnedProgrammaticDecomposeIgnored, SUPERSESSION_SIMILARITY_THRESHOLD, SUPERSESSION_MIN_AGE_GAP_MS, SUPERSESSION_BOOST_FACTOR, SUPERSESSION_MAX_WINDOW, EMBEDDINGS_DEGRADED_EMPTY_NO_LEXICAL;
23231
23287
  var init_searchTool = __esm({
23232
23288
  "src/lib/memoryVault/searchTool.ts"() {
23233
23289
  "use strict";
@@ -23245,6 +23301,7 @@ var init_searchTool = __esm({
23245
23301
  init_bm25();
23246
23302
  init_decomposeQuery();
23247
23303
  init_lruCache();
23304
+ nowMs2 = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
23248
23305
  warnedProgrammaticDecomposeIgnored = false;
23249
23306
  SUPERSESSION_SIMILARITY_THRESHOLD = 0.7;
23250
23307
  SUPERSESSION_MIN_AGE_GAP_MS = 30 * 24 * 60 * 60 * 1e3;
@@ -30602,6 +30659,7 @@ ${TRUNCATION_NOTICE}` : formatted;
30602
30659
  // src/lib/memory/reflect.ts
30603
30660
  init_clientConfig();
30604
30661
  init_logger();
30662
+ init_taskType();
30605
30663
  init_portalLlm();
30606
30664
  init_recall();
30607
30665
  var DEFAULT_BASE_URL2 = BASE_URL;
@@ -30647,11 +30705,13 @@ Respond with ONLY a single JSON object conforming to this JSON Schema, with no p
30647
30705
  ${JSON.stringify(
30648
30706
  options.responseSchema
30649
30707
  )}` : basePrompt;
30650
- const userMessage = `Question:
30651
- ${trimmed}
30652
-
30653
- Memories (use only these as evidence):
30654
- ${evidence}`;
30708
+ const userMessage = [
30709
+ `Question:
30710
+ ${trimmed}`,
30711
+ ...options.userInstructions ? [options.userInstructions] : [],
30712
+ `Memories (use only these as evidence):
30713
+ ${evidence}`
30714
+ ].join("\n\n");
30655
30715
  const maxTokens = options.maxTokens ?? DEFAULT_MAX_TOKENS;
30656
30716
  const fetchImpl = options.fetchFn ?? fetch;
30657
30717
  const log = getLogger();
@@ -30665,7 +30725,13 @@ ${evidence}`;
30665
30725
  try {
30666
30726
  response = await fetchImpl(`${baseUrl}/api/v1/chat/completions`, {
30667
30727
  method: "POST",
30668
- headers: { ...authHeaders, "Content-Type": "application/json" },
30728
+ // No task type unless the caller named one — an undeclared task adds no
30729
+ // header at all, which is what keeps user-facing reflect() unlabelled.
30730
+ headers: {
30731
+ ...authHeaders,
30732
+ ...taskTypeHeader(options.taskType),
30733
+ "Content-Type": "application/json"
30734
+ },
30669
30735
  body: JSON.stringify({
30670
30736
  model,
30671
30737
  // Modern OpenAI field; the portal reads only `max_completion_tokens`
@@ -31117,10 +31183,15 @@ async function synthesizeFacet(facet, ctx, options, prior) {
31117
31183
  limit,
31118
31184
  types: ["fact"],
31119
31185
  maxTokens: DEFAULT_FACET_MAX_TOKENS,
31120
- // Profile-facet synthesis is a background op, so it marks its own prompt.
31121
- // reflect() itself must NOT mark — it also serves the user's own questions,
31122
- // and that traffic is chat, not an internal flow. See ../internalFlowMarker.
31123
- systemPrompt: withInternalFlowMarker(buildFacetSystemPrompt(facet)),
31186
+ // Profile-facet synthesis is a background op, so it marks its own prompt AND
31187
+ // names its own task. reflect() itself must do neither — it also serves the
31188
+ // user's own questions, and that traffic is chat, not an internal flow. See
31189
+ // ../internalFlowMarker and ReflectOptions.taskType.
31190
+ taskType: "memory_profile_synth",
31191
+ systemPrompt: withInternalFlowMarker(FACET_SYSTEM_PROMPT),
31192
+ // The facet's label/guidance/response-fields, on the user turn — what keeps
31193
+ // the system half fixed and therefore server-ownable.
31194
+ userInstructions: buildFacetUserInstructions(facet),
31124
31195
  responseSchema: facetResponseSchema(facet.key),
31125
31196
  memories
31126
31197
  });
@@ -31178,19 +31249,22 @@ function fallbackSection(facet, prior) {
31178
31249
  stale: true
31179
31250
  };
31180
31251
  }
31181
- function buildFacetSystemPrompt(facet) {
31182
- const structuredField = FACET_STRUCTURED_RESPONSE_HINT[facet.key] ?? "";
31183
- 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.
31184
-
31185
- Task for this section:
31186
- ${facet.guidance}
31252
+ 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.
31187
31253
 
31188
31254
  Rules:
31189
31255
  - Ground every claim in the supplied memories \u2014 never invent, infer beyond, or embellish what they support.
31190
31256
  - If the memories don't cover this section, return an empty summary with hasEvidence=false. Do not pad or guess.
31191
31257
  - Write in third person about the person, in a natural voice suitable for a public profile (no "I"/"you", no name repetition).
31192
31258
  - Be concise and specific; no preamble, hedging, or meta-commentary.
31193
- - Respond as JSON: { "summary": <the section text, or "">, "hasEvidence": <true|false>${structuredField} }.`;
31259
+ - Respond as JSON in exactly the shape the user turn states, with no extra fields.`;
31260
+ function buildFacetUserInstructions(facet) {
31261
+ const structuredField = FACET_STRUCTURED_RESPONSE_HINT[facet.key] ?? "";
31262
+ return `Section: "${facet.label}"
31263
+
31264
+ Task for this section:
31265
+ ${facet.guidance}
31266
+
31267
+ Respond as JSON: { "summary": <the section text, or "">, "hasEvidence": <true|false>${structuredField} }.`;
31194
31268
  }
31195
31269
  function extractFacetText(structured) {
31196
31270
  if (structured && typeof structured === "object") {
@@ -4698,6 +4698,51 @@ interface EntityOperationsContext {
4698
4698
  allowUnscopedRows?: boolean;
4699
4699
  }
4700
4700
 
4701
+ /**
4702
+ * Class-B task types — the vocabulary of the `X-Anuma-Task-Type` header.
4703
+ *
4704
+ * A Class-B flow has ONE fixed purpose and no conversation: extract memories
4705
+ * from a snippet, pick a topic, consolidate duplicates, classify a query. Today
4706
+ * each of those builds its own system prompt on the client and sends it as a
4707
+ * `role:system` message, which makes the prompt a suggestion rather than a
4708
+ * contract — a caller can reword it into "you are a general assistant" and get
4709
+ * general chat at internal-endpoint prices.
4710
+ *
4711
+ * The fix is that the SERVER owns the text and the client only names the task.
4712
+ * This header is the name. The portal allowlists it against its own copy of this
4713
+ * enum (ai-portal `internal/systemprompt/tasks.go`) and DROPS anything it does
4714
+ * not recognise, so an unknown value degrades to "no task declared" rather than
4715
+ * failing the request.
4716
+ *
4717
+ * ⚠ These string values are a wire contract with ai-portal. Adding one here
4718
+ * before the portal knows it is harmless (it is dropped); renaming or removing
4719
+ * one silently stops that flow being counted. Keep them in step with
4720
+ * `AllTaskTypes` on the portal side, and see ai-portal
4721
+ * `docs/BACKEND_OWNED_TASK_PROMPTS.md` for the rollout.
4722
+ *
4723
+ * ⚠ RELEASE ORDER. This union is a PUBLIC type, so a split is breaking in both
4724
+ * directions and has to be sequenced:
4725
+ *
4726
+ * 1. merge + publish this package;
4727
+ * 2. bump `@anuma/sdk` in the consumer and switch its call sites to the new
4728
+ * names in the same commit.
4729
+ *
4730
+ * A consumer on the OLD version cannot compile the new names, and a consumer on
4731
+ * the NEW version cannot compile the retired ones (`app_inspiration` is gone
4732
+ * here, replaced by the three `app_inspiration_*` names). Both halves of that
4733
+ * are type-only — the wire itself always degrades safely, because the portal
4734
+ * drops any value it does not recognise. Concretely: ai-memoryless-client#5782
4735
+ * is blocked on this publish for exactly that reason.
4736
+ *
4737
+ * Deliberately ABSENT: agent personas, `reflect()` when it answers the user, and
4738
+ * the app-idea chat. Those carry an open conversation with the user, so a fixed
4739
+ * server-owned prompt would be wrong rather than merely premature — the same
4740
+ * boundary {@link INTERNAL_FLOW_MARKER} draws.
4741
+ *
4742
+ * @public
4743
+ */
4744
+ 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";
4745
+
4701
4746
  /**
4702
4747
  * Shared portal LLM call for the memory pipeline.
4703
4748
  *
@@ -5641,6 +5686,17 @@ interface RecallDiagnostics {
5641
5686
  * never the cost.
5642
5687
  */
5643
5688
  vaultRowsDecrypted?: number;
5689
+ /**
5690
+ * Rows the fact lane had to re-embed through the portal because their stored
5691
+ * vector was unusable — stale `embedding_model`, wrong dimension, or
5692
+ * unparseable. Absent when the fact lane didn't run.
5693
+ *
5694
+ * Expected to be 0 on a healthy vault: rows are embedded at write time and the
5695
+ * re-embed writes the current model back, so a persistently non-zero value
5696
+ * means the writeback is not sticking and every turn is paying for it. On the
5697
+ * LEGACY read path this batch is uncapped, so it can be the whole vault.
5698
+ */
5699
+ vaultRowsEmbedded?: number;
5644
5700
  /** Facts the fact lane returned (post-dedupe, pre-fusion). */
5645
5701
  factCount: number;
5646
5702
  /** Chunks the chunk lane returned (post-dedupe, pre-fusion). */
@@ -5653,6 +5709,37 @@ interface RecallDiagnostics {
5653
5709
  prep: number;
5654
5710
  /** Vault fact-lane search (`searchVaultMemoriesWithSize`). */
5655
5711
  factLane: number;
5712
+ /**
5713
+ * The cross-encoder's share of {@link factLane} — wall-clock spent inside
5714
+ * `rerankPairs`. Billed even when the CE threw partway through, because a
5715
+ * rerank that burned three seconds and then failed still cost them.
5716
+ *
5717
+ * A SUBSET of `factLane`, not a sibling: read `factLane - rerank` for
5718
+ * everything else the lane did (query embed, vault read, fused ranking).
5719
+ * 0 when the CE did not run; `reranked` is what distinguishes that from a
5720
+ * rerank that cost nothing.
5721
+ *
5722
+ * Exists because #845 spent three rounds arguing about which stage inside
5723
+ * the fact lane dominated — whole-vault read, then admission window, then
5724
+ * the CE — with one aggregate number for all of them. Every hypothesis was
5725
+ * an inference; this makes the question a query.
5726
+ */
5727
+ rerank: number;
5728
+ /**
5729
+ * The query embed's share of {@link factLane} — one portal round trip.
5730
+ *
5731
+ * Also a SUBSET of `factLane`. Together with `rerank` this makes the lane
5732
+ * decomposable: `factLane - queryEmbed - rerank` is the local work (vault
5733
+ * read, decrypt, BM25/cosine fusion), and each of the three implies a
5734
+ * completely different fix.
5735
+ *
5736
+ * 0 when the lane returned before embedding, which includes every empty
5737
+ * vault — `prepareVaultCandidates` short-circuits first. That is why the
5738
+ * fast `vault_size = 0` population never established a baseline for this
5739
+ * cost, and why the ~850ms floor on the smallest NON-empty vaults had no
5740
+ * attributable owner.
5741
+ */
5742
+ queryEmbed: number;
5656
5743
  /** Chunk-lane search (`searchChunksOp`). */
5657
5744
  chunkLane: number;
5658
5745
  /** Cross-lane RRF fusion + provenance dedup after both lanes. */
@@ -8757,6 +8844,28 @@ interface ReflectOptions extends RecallOptions, PortalLlmAuth {
8757
8844
  maxTokens?: number;
8758
8845
  /** Override the grounding system prompt. */
8759
8846
  systemPrompt?: string;
8847
+ /**
8848
+ * Extra caller instruction to carry on the USER turn, between the question and
8849
+ * the evidence block (see the `userMessage` assembly below). This is the slot a
8850
+ * background caller uses to keep its per-request data OUT of the system message
8851
+ * without colliding with the numbered evidence list — profile-facet synthesis
8852
+ * puts its section label, guidance and response-field hint here so its system
8853
+ * half can stay fixed and server-ownable.
8854
+ */
8855
+ userInstructions?: string;
8856
+ /**
8857
+ * Class-B task name for the `X-Anuma-Task-Type` header, or nothing.
8858
+ *
8859
+ * Deliberately OPTIONAL and unset by default. reflect() also answers the user's
8860
+ * OWN question, and that traffic is chat, not an internal flow — declaring a
8861
+ * task type unconditionally here would put an internal-flow name on real
8862
+ * conversation, which is the same boundary `INTERNAL_FLOW_MARKER` draws
8863
+ * (reflect is deliberately unmarked; its background caller marks its own
8864
+ * prompt — see ../internalFlowMarker.ts). So the name is per call: only a
8865
+ * caller with ONE fixed purpose passes
8866
+ * one, and today that is profile-facet synthesis (`memory_profile_synth`).
8867
+ */
8868
+ taskType?: TaskType;
8760
8869
  /** Endpoint for the answer LLM. */
8761
8870
  baseUrl?: string;
8762
8871
  /** Override fetch (for tests). */