@anuma/sdk 4.2.1 → 5.0.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.
@@ -20612,11 +20612,11 @@ async function decomposeQuery(query, options) {
20612
20612
  userMessage: `Classify the following memory query and decompose if composite. Respond with JSON only \u2014 do not answer the question, do not ask for clarification.
20613
20613
 
20614
20614
  Query: ${trimmed}`,
20615
- // Decompose runs on the recall hot path — tighter than the
20615
+ // Decompose runs ahead of recall in the tool layer — tighter than the
20616
20616
  // portalLlm default (consolidate/extract can wait longer).
20617
20617
  timeoutMs: 2e4,
20618
20618
  // No internal retry: a failure degrades to the safe `{specific,[query]}`
20619
- // fallback below, so retrying would only add latency on the hot path.
20619
+ // fallback below. The agent/tool loop is the retry surface (719/B4).
20620
20620
  maxAttempts: 1,
20621
20621
  tag: "memory/decompose",
20622
20622
  ...options.fetchFn && { fetchFn: options.fetchFn }
@@ -20628,12 +20628,28 @@ Query: ${trimmed}`,
20628
20628
  if (parsed === null) return fallback;
20629
20629
  return validate(parsed, trimmed) ?? fallback;
20630
20630
  }
20631
+ function normalizeSubQueries(raw) {
20632
+ if (!raw || raw.length === 0) return [];
20633
+ const seen = /* @__PURE__ */ new Set();
20634
+ const out = [];
20635
+ for (const s of raw) {
20636
+ if (typeof s !== "string") continue;
20637
+ const t = s.trim();
20638
+ if (t.length === 0) continue;
20639
+ const key = t.toLowerCase();
20640
+ if (seen.has(key)) continue;
20641
+ seen.add(key);
20642
+ out.push(t);
20643
+ if (out.length >= MAX_SUB_QUERIES) break;
20644
+ }
20645
+ return out;
20646
+ }
20631
20647
  function validate(parsed, originalQuery) {
20632
20648
  if (typeof parsed !== "object" || parsed === null) return null;
20633
20649
  const obj = parsed;
20634
20650
  const mode = obj.mode === "composite" ? "composite" : "specific";
20635
20651
  if (!Array.isArray(obj.subQueries)) return null;
20636
- const subQueries = obj.subQueries.filter((s) => typeof s === "string").map((s) => s.trim()).filter((s) => s.length > 0).slice(0, MAX_SUB_QUERIES);
20652
+ const subQueries = normalizeSubQueries(obj.subQueries);
20637
20653
  if (subQueries.length === 0) return null;
20638
20654
  if (mode === "specific") {
20639
20655
  return { mode: "specific", subQueries: [originalQuery] };
@@ -21212,12 +21228,12 @@ __export(recall_exports, {
21212
21228
  function flagsForBudget(budget) {
21213
21229
  switch (budget) {
21214
21230
  case "high":
21215
- return { rerank: true, decompose: true, traverse: true };
21231
+ return { rerank: true, traverse: true };
21216
21232
  case "mid":
21217
- return { rerank: true, decompose: false, traverse: false };
21233
+ return { rerank: true, traverse: false };
21218
21234
  case "low":
21219
21235
  default:
21220
- return { rerank: false, decompose: false, traverse: false };
21236
+ return { rerank: false, traverse: false };
21221
21237
  }
21222
21238
  }
21223
21239
  function dedupeBy(items, ...keys) {
@@ -21236,10 +21252,10 @@ function dedupeBy(items, ...keys) {
21236
21252
  async function recall(query, ctx, options = {}) {
21237
21253
  const types = options.types ?? ["fact"];
21238
21254
  const limit = options.limit ?? DEFAULT_LIMIT;
21239
- const requestedBudget = options.budget ?? DEFAULT_BUDGET;
21240
- const flags = flagsForBudget(requestedBudget);
21241
- const decomposeAvailable = flags.decompose && !!options.decomposeOptions;
21242
- const usedBudget = requestedBudget === "high" && !decomposeAvailable ? "mid" : requestedBudget;
21255
+ const usedBudget = options.budget ?? DEFAULT_BUDGET;
21256
+ const flags = flagsForBudget(usedBudget);
21257
+ const normalizedFacets = normalizeSubQueries(options.subQueries);
21258
+ const subQueries = normalizedFacets.length >= 2 ? normalizedFacets : void 0;
21243
21259
  const t0 = nowMs();
21244
21260
  let prepMs = 0;
21245
21261
  let factLaneMs = 0;
@@ -21260,7 +21276,9 @@ async function recall(query, ctx, options = {}) {
21260
21276
  if (flags.rerank && factResults.length > 0 && hadV2Head && !didRerank) {
21261
21277
  degraded.push("rerank-unavailable");
21262
21278
  }
21263
- if (flags.decompose && !decomposeAvailable) degraded.push("decompose-unavailable");
21279
+ if (usedBudget === "high" && options.decomposeOptions && !subQueries && !options.graphRefine) {
21280
+ degraded.push("decompose-moved");
21281
+ }
21264
21282
  if (embeddingsUnavailable) degraded.push("embeddings-unavailable");
21265
21283
  const diagnostics = {
21266
21284
  usedBudget,
@@ -21373,10 +21391,8 @@ async function recall(query, ctx, options = {}) {
21373
21391
  },
21374
21392
  ...options.rrfK !== void 0 && { rrfK: options.rrfK },
21375
21393
  ...options.decryptLast !== void 0 && { decryptLast: options.decryptLast },
21376
- ...decomposeAvailable && {
21377
- decompose: "llm",
21378
- decomposeOptions: options.decomposeOptions
21379
- },
21394
+ // 719/B4 — composite facets arrive pre-built; no LLM call here.
21395
+ ...subQueries && { subQueries },
21380
21396
  ...options.scopes && { scopes: options.scopes },
21381
21397
  ...options.folderId !== void 0 && { folderId: options.folderId },
21382
21398
  ...options.factTypes?.length && { factTypes: options.factTypes },
@@ -21613,6 +21629,7 @@ var init_recall = __esm({
21613
21629
  init_logger();
21614
21630
  init_constants();
21615
21631
  init_embeddings();
21632
+ init_decomposeQuery();
21616
21633
  init_searchTool();
21617
21634
  init_graphTraversal();
21618
21635
  init_observationTrend();
@@ -22558,52 +22575,51 @@ async function rankPreparedVaultCandidates(query, prepared, embeddingOptions, se
22558
22575
  ...searchOptions?.rrfK !== void 0 && { rrfK: searchOptions.rrfK },
22559
22576
  ...searchOptions?.factTypeWeights && { factTypeWeights: searchOptions.factTypeWeights }
22560
22577
  };
22561
- if (useFusion && !embeddingsUnavailable && searchOptions?.decompose === "llm" && searchOptions.decomposeOptions) {
22562
- const decomp = await decomposeQuery(query, searchOptions.decomposeOptions);
22563
- if (decomp.mode === "composite") {
22564
- let subEmbeddings;
22565
- try {
22566
- subEmbeddings = await generateEmbeddings(decomp.subQueries, embeddingOptions);
22567
- } catch (err) {
22568
- getLogger().warn(
22569
- "memoryVault: sub-query embedding failed \u2014 falling back to single-query ranking: " + (err instanceof Error ? err.message : String(err))
22570
- );
22571
- subEmbeddings = [];
22572
- }
22573
- const subEmbeddingsUsable = decomp.subQueries.length > 0 && subEmbeddings.length === decomp.subQueries.length && subEmbeddings.every((v) => v.length > 0 && v.length === queryEmbedding.length);
22574
- if (subEmbeddings.length > 0 && !subEmbeddingsUsable) {
22575
- getLogger().warn(
22576
- `memoryVault: sub-query embedding returned ${subEmbeddings.length} vectors (dims ${subEmbeddings.map((v) => v.length).join(",")}) for ${decomp.subQueries.length} sub-queries against a ${queryEmbedding.length}-dim query \u2014 falling back to single-query ranking`
22577
- );
22578
- }
22579
- if (subEmbeddingsUsable) {
22580
- const subQueries = decomp.subQueries.map((sq, i) => ({
22581
- query: sq,
22582
- embedding: subEmbeddings[i]
22583
- }));
22584
- const v2HeadStats = { hadResults: false };
22585
- const composite = await rankComposite(query, queryEmbedding, subQueries, embeddedItems, {
22586
- limit,
22587
- minSimilarity,
22588
- rerank: !!searchOptions.rerank,
22589
- ...searchOptions.rerankTopN !== void 0 && {
22590
- rerankTopN: searchOptions.rerankTopN
22591
- },
22592
- ...searchOptions.ceWeight !== void 0 && { ceWeight: searchOptions.ceWeight },
22593
- ...searchOptions.mmr !== void 0 && { mmr: searchOptions.mmr },
22594
- ...tuning,
22595
- ...searchOptions.entityRanking && { entityRanking: searchOptions.entityRanking },
22596
- ...searchOptions.temporalRanking && { temporalRanking: searchOptions.temporalRanking },
22597
- rerankStats,
22598
- v2HeadStats
22599
- });
22600
- return stampTimestamps({
22601
- results: composite,
22602
- vaultSize,
22603
- reranked: rerankStats.applied,
22604
- hadV2Head: v2HeadStats.hadResults
22605
- });
22606
- }
22578
+ const normalizedFacets = normalizeSubQueries(searchOptions?.subQueries);
22579
+ const facetQueries = normalizedFacets.length >= 2 ? normalizedFacets : void 0;
22580
+ if (useFusion && !embeddingsUnavailable && facetQueries) {
22581
+ let subEmbeddings;
22582
+ try {
22583
+ subEmbeddings = await generateEmbeddings(facetQueries, embeddingOptions);
22584
+ } catch (err) {
22585
+ getLogger().warn(
22586
+ "memoryVault: sub-query embedding failed \u2014 falling back to single-query ranking: " + (err instanceof Error ? err.message : String(err))
22587
+ );
22588
+ subEmbeddings = [];
22589
+ }
22590
+ const subEmbeddingsUsable = facetQueries.length > 0 && subEmbeddings.length === facetQueries.length && subEmbeddings.every((v) => v.length > 0 && v.length === queryEmbedding.length);
22591
+ if (subEmbeddings.length > 0 && !subEmbeddingsUsable) {
22592
+ getLogger().warn(
22593
+ `memoryVault: sub-query embedding returned ${subEmbeddings.length} vectors (dims ${subEmbeddings.map((v) => v.length).join(",")}) for ${facetQueries.length} sub-queries against a ${queryEmbedding.length}-dim query \u2014 falling back to single-query ranking`
22594
+ );
22595
+ }
22596
+ if (subEmbeddingsUsable) {
22597
+ const subQueries = facetQueries.map((sq, i) => ({
22598
+ query: sq,
22599
+ embedding: subEmbeddings[i]
22600
+ }));
22601
+ const v2HeadStats = { hadResults: false };
22602
+ const composite = await rankComposite(query, queryEmbedding, subQueries, embeddedItems, {
22603
+ limit,
22604
+ minSimilarity,
22605
+ rerank: !!searchOptions?.rerank,
22606
+ ...searchOptions?.rerankTopN !== void 0 && {
22607
+ rerankTopN: searchOptions.rerankTopN
22608
+ },
22609
+ ...searchOptions?.ceWeight !== void 0 && { ceWeight: searchOptions.ceWeight },
22610
+ ...searchOptions?.mmr !== void 0 && { mmr: searchOptions.mmr },
22611
+ ...tuning,
22612
+ ...searchOptions?.entityRanking && { entityRanking: searchOptions.entityRanking },
22613
+ ...searchOptions?.temporalRanking && { temporalRanking: searchOptions.temporalRanking },
22614
+ rerankStats,
22615
+ v2HeadStats
22616
+ });
22617
+ return stampTimestamps({
22618
+ results: composite,
22619
+ vaultSize,
22620
+ reranked: rerankStats.applied,
22621
+ hadV2Head: v2HeadStats.hadResults
22622
+ });
22607
22623
  }
22608
22624
  }
22609
22625
  if (useFusion && searchOptions?.rerank) {
@@ -22663,6 +22679,12 @@ async function searchVaultMemoriesWithSize(query, vaultCtx, embeddingOptions, ca
22663
22679
  rankedOnCosine: false
22664
22680
  };
22665
22681
  }
22682
+ if (searchOptions?.decompose === "llm" && normalizeSubQueries(searchOptions.subQueries).length < 2 && !warnedProgrammaticDecomposeIgnored) {
22683
+ warnedProgrammaticDecomposeIgnored = true;
22684
+ getLogger().warn(
22685
+ 'memoryVault: decompose:"llm" is ignored by searchVaultMemories \u2014 pass subQueries or use createMemoryVaultSearchTool / createRecallTool (719/B4)'
22686
+ );
22687
+ }
22666
22688
  const prepared = await prepareVaultCandidates(
22667
22689
  query,
22668
22690
  vaultCtx,
@@ -22767,7 +22789,8 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
22767
22789
  return "Error: A search query is required.";
22768
22790
  }
22769
22791
  try {
22770
- const budget = searchOptions?.decompose === "llm" && searchOptions.decomposeOptions ? "high" : searchOptions?.rerank ? "mid" : "low";
22792
+ const wantsDecompose = searchOptions?.decompose === "llm" && !!searchOptions.decomposeOptions;
22793
+ const budget = wantsDecompose ? "high" : searchOptions?.rerank ? "mid" : "low";
22771
22794
  const folderId = searchOptions?.folderId ?? argFolderId;
22772
22795
  if (searchOptions?.useFusion === false) {
22773
22796
  const { results: legacy, embeddingsUnavailable } = await searchVaultMemoriesWithSize(
@@ -22791,6 +22814,13 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
22791
22814
  legacy.map((r) => ({ id: r.uniqueId, content: r.content, score: r.similarity }))
22792
22815
  );
22793
22816
  }
22817
+ let subQueries;
22818
+ if (wantsDecompose && searchOptions?.decomposeOptions) {
22819
+ const decomp = await decomposeQuery(query, searchOptions.decomposeOptions);
22820
+ if (decomp.mode === "composite" && decomp.subQueries.length >= 2) {
22821
+ subQueries = decomp.subQueries;
22822
+ }
22823
+ }
22794
22824
  const { recall: recall2 } = await Promise.resolve().then(() => (init_recall(), recall_exports));
22795
22825
  let recallDegraded = [];
22796
22826
  const result = await recall2(
@@ -22807,9 +22837,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
22807
22837
  ...tuningForward,
22808
22838
  ...folderId !== void 0 && { folderId },
22809
22839
  ...searchOptions?.scopes && { scopes: searchOptions.scopes },
22810
- ...searchOptions?.decompose === "llm" && searchOptions.decomposeOptions && {
22811
- decomposeOptions: searchOptions.decomposeOptions
22812
- }
22840
+ ...subQueries && { subQueries }
22813
22841
  }
22814
22842
  );
22815
22843
  if (result.vaultSize === 0) {
@@ -22839,7 +22867,7 @@ ${formatted}`;
22839
22867
  }
22840
22868
  };
22841
22869
  }
22842
- var SUPERSESSION_SIMILARITY_THRESHOLD, SUPERSESSION_MIN_AGE_GAP_MS, SUPERSESSION_BOOST_FACTOR, SUPERSESSION_MAX_WINDOW, EMBEDDINGS_DEGRADED_EMPTY_NO_LEXICAL;
22870
+ var warnedProgrammaticDecomposeIgnored, SUPERSESSION_SIMILARITY_THRESHOLD, SUPERSESSION_MIN_AGE_GAP_MS, SUPERSESSION_BOOST_FACTOR, SUPERSESSION_MAX_WINDOW, EMBEDDINGS_DEGRADED_EMPTY_NO_LEXICAL;
22843
22871
  var init_searchTool = __esm({
22844
22872
  "src/lib/memoryVault/searchTool.ts"() {
22845
22873
  "use strict";
@@ -22857,6 +22885,7 @@ var init_searchTool = __esm({
22857
22885
  init_bm25();
22858
22886
  init_decomposeQuery();
22859
22887
  init_lruCache();
22888
+ warnedProgrammaticDecomposeIgnored = false;
22860
22889
  SUPERSESSION_SIMILARITY_THRESHOLD = 0.7;
22861
22890
  SUPERSESSION_MIN_AGE_GAP_MS = 30 * 24 * 60 * 60 * 1e3;
22862
22891
  SUPERSESSION_BOOST_FACTOR = 0.8;
@@ -29733,6 +29762,7 @@ function rankProfileCandidates(inputs, now = Date.now(), options = {}) {
29733
29762
  init_recall();
29734
29763
 
29735
29764
  // src/lib/memory/recallTool.ts
29765
+ init_decomposeQuery();
29736
29766
  init_recall();
29737
29767
  init_recallConstants();
29738
29768
  var DEFAULT_LIMIT2 = 8;
@@ -29830,7 +29860,7 @@ function createRecallTool(ctx, toolOptions, callbacks) {
29830
29860
  type: "function",
29831
29861
  function: {
29832
29862
  name: RECALL_TOOL_NAME,
29833
- description: "Search the user's memory across stored facts/preferences and past conversation excerpts. Returns a unified ranked list \u2014 facts carry an `id` you can reference; conversation excerpts carry a date and role. Use this whenever the user's question may relate to anything previously discussed or saved (preferences, prior decisions, past topics). Phrase the query naturally.",
29863
+ description: `Search the user's memory across stored facts/preferences and past conversation excerpts. Returns a unified ranked list \u2014 facts carry an \`id\` you can reference; conversation excerpts carry a date and role. Use this whenever the user's question may relate to anything previously discussed or saved (preferences, prior decisions, past topics). Phrase the query naturally. For multi-faceted / overview questions ("tell me about the user", "what's my tech stack"), prefer several targeted searches \u2014 one facet each (e.g. name, work, hobbies) \u2014 over a single broad query; fuse the results yourself.`,
29834
29864
  arguments: {
29835
29865
  type: "object",
29836
29866
  properties: {
@@ -29885,6 +29915,13 @@ function createRecallTool(ctx, toolOptions, callbacks) {
29885
29915
  let surfaced = 0;
29886
29916
  try {
29887
29917
  let recallDegraded = [];
29918
+ let subQueries;
29919
+ if (defaultBudget === "high" && toolOptions?.decomposeOptions) {
29920
+ const decomp = await decomposeQuery(query, toolOptions.decomposeOptions);
29921
+ if (decomp.mode === "composite" && decomp.subQueries.length >= 2) {
29922
+ subQueries = decomp.subQueries;
29923
+ }
29924
+ }
29888
29925
  const recallOpts = {
29889
29926
  onDiagnostics: (d) => {
29890
29927
  recallDegraded = d.degraded;
@@ -29898,10 +29935,12 @@ function createRecallTool(ctx, toolOptions, callbacks) {
29898
29935
  ...toolOptions?.excludeConversationId && {
29899
29936
  excludeConversationId: toolOptions.excludeConversationId
29900
29937
  },
29901
- ...toolOptions?.decomposeOptions && {
29902
- decomposeOptions: toolOptions.decomposeOptions
29903
- },
29904
- ...toolOptions?.now !== void 0 && { now: toolOptions.now }
29938
+ // Do NOT forward decomposeOptions into recall() — rewrite already
29939
+ // ran above, and forwarding would falsely trip `decompose-moved`
29940
+ // on every specific-mode high-budget call. graphRefine auth can
29941
+ // be threaded later when the tool opts into that path.
29942
+ ...toolOptions?.now !== void 0 && { now: toolOptions.now },
29943
+ ...subQueries && { subQueries }
29905
29944
  };
29906
29945
  const result = await recall(query, ctx, recallOpts);
29907
29946
  if (callbacks?.onChunksRetrieved) {
@@ -4945,15 +4945,26 @@ interface MemoryVaultSearchOptions {
4945
4945
  /** RRF smoothing constant for lane fusion. Default 60. */
4946
4946
  rrfK?: number;
4947
4947
  /**
4948
- * LLM-based query decomposition for composite/abstract queries. When set,
4949
- * each query is classified + (if composite) decomposed into 3–5 facet
4950
- * sub-queries via gpt-5-mini, then ranked via {@link rankComposite}.
4951
- * Requires `decomposeOptions` (auth) when set to "llm".
4948
+ * Pre-decomposed facet queries for the composite ranker (719/B4). When
4949
+ * ≥2 are supplied (and embeddings are available), runs {@link rankComposite}
4950
+ * over them — no LLM call inside the search path. Callers that want LLM
4951
+ * rewrite (e.g. `createRecallTool`) call `decomposeQuery` first and pass
4952
+ * the facets here.
4953
+ */
4954
+ subQueries?: string[];
4955
+ /**
4956
+ * @deprecated 719/B4 — ignored by {@link searchVaultMemories} /
4957
+ * {@link searchVaultMemoriesWithSize}. Pass {@link MemoryVaultSearchOptions.subQueries}
4958
+ * (or use `createRecallTool`). The legacy {@link createMemoryVaultSearchTool}
4959
+ * executor still honors `decompose: "llm"` + `decomposeOptions` for eval
4960
+ * parity, then forwards facets into the LLM-free search path.
4952
4961
  */
4953
4962
  decompose?: "off" | "llm";
4954
- /** Auth + endpoint for the decomposition LLM call. Required when
4955
- * decompose="llm". Auth is the dual pattern — one of `apiKey` /
4956
- * `getToken`; see {@link PortalLlmAuth}. */
4963
+ /**
4964
+ * @deprecated 719/B4 — see `decompose`. Ignored on the programmatic search
4965
+ * path; tool-layer rewrite still reads this from the search-tool options.
4966
+ * Prefer `RecallToolOptions.decomposeOptions` with `createRecallTool`.
4967
+ */
4957
4968
  decomposeOptions?: PortalLlmAuth & {
4958
4969
  baseUrl?: string;
4959
4970
  model?: string;
@@ -5118,9 +5129,11 @@ type MemoryKind = "fact" | "chunk";
5118
5129
  * Budget controls retrieval depth/cost. Higher budgets enable more
5119
5130
  * candidate sources and the cross-encoder reranker.
5120
5131
  *
5121
- * - `low`: cosine + BM25 fusion only. No reranker. Mobile default.
5122
- * - `mid`: + recency boost. Demo default.
5123
- * - `high`: + cross-encoder rerank stage. Demo `budget: high` path.
5132
+ * - `low`: cosine + BM25 + recency. No reranker. Mobile default.
5133
+ * - `mid`: + cross-encoder rerank.
5134
+ * - `high`: + multi-hop graph traversal. LLM-free — query decomposition
5135
+ * (when wanted) lives in the tool/agent layer ({@link createRecallTool}),
5136
+ * not inside `recall()` itself (719/B4).
5124
5137
  */
5125
5138
  type Budget = "low" | "mid" | "high";
5126
5139
  interface ScoreBreakdown {
@@ -5220,10 +5233,28 @@ interface RecallOptions {
5220
5233
  /** Drop results below this score. Default: 0.1 for facts, 0.5 for chunks (mirrors today's defaults). */
5221
5234
  minScore?: number;
5222
5235
  /**
5223
- * Auth + endpoint for the LLM-based query decomposition pass. Without
5224
- * these, decompose is skipped even at `budget: 'high'`. Mirrors the
5225
- * shape used by `searchVaultMemories`. Auth is the dual pattern — one
5226
- * of `apiKey` / `getToken` is required; see {@link PortalLlmAuth}.
5236
+ * Pre-decomposed facet queries for the composite ranker. When ≥2 are
5237
+ * supplied, the vault lane runs `rankComposite` over them (no LLM call
5238
+ * inside `recall()` — 719/B4). Callers that still want LLM rewrite
5239
+ * (e.g. {@link createRecallTool} at `budget: 'high'`) call
5240
+ * `decomposeQuery` themselves and pass the result here. A single entry
5241
+ * (or omitting this) keeps the single-query path.
5242
+ */
5243
+ subQueries?: string[];
5244
+ /**
5245
+ * Auth + endpoint for optional LLM helpers that reuse portal auth —
5246
+ * currently {@link RecallOptions.graphRefine} neighbor selection.
5247
+ * Query decomposition is **not** driven by this field inside `recall()`
5248
+ * (719/B4); {@link createRecallTool} reads the same shape from
5249
+ * `RecallToolOptions.decomposeOptions` for tool-layer rewrite.
5250
+ *
5251
+ * Callers that still pass `{ budget: 'high', decomposeOptions }` without
5252
+ * {@link RecallOptions.subQueries} keep compiling but no longer rewrite —
5253
+ * `recall()` emits `decompose-moved` on {@link RecallDiagnostics.degraded}
5254
+ * so upgrades without a changelog read still leave a telemetry breadcrumb.
5255
+ *
5256
+ * Auth is the dual pattern — one of `apiKey` / `getToken` is required;
5257
+ * see {@link PortalLlmAuth}.
5227
5258
  */
5228
5259
  decomposeOptions?: PortalLlmAuth & {
5229
5260
  baseUrl?: string;
@@ -5327,9 +5358,22 @@ type RecallDegradation =
5327
5358
  /** Rerank was requested (budget mid/high) but the cross-encoder didn't run
5328
5359
  * this call — unavailable (e.g. React Native) or a transient failure. */
5329
5360
  "rerank-unavailable"
5330
- /** `budget: 'high'` requested but no `decomposeOptions`, so query
5331
- * decomposition was skipped and the budget downgraded to mid. */
5361
+ /**
5362
+ * @deprecated 719/B4 moved query decomposition out of `recall()`. Budget
5363
+ * `high` no longer requires `decomposeOptions` and this signal is never
5364
+ * emitted. Prefer `"decompose-moved"` for the upgrade breadcrumb. Kept in
5365
+ * the union so existing telemetry consumers stay typed.
5366
+ */
5332
5367
  | "decompose-unavailable"
5368
+ /**
5369
+ * `budget: 'high'` with `decomposeOptions` set but no usable
5370
+ * {@link RecallOptions.subQueries}. Pre-B4 this call shape triggered LLM
5371
+ * rewrite inside `recall()`; post-B4 it stays on the single-query high
5372
+ * path (rerank + graph) and callers must pass facets (or use
5373
+ * {@link createRecallTool}). Surfaced so silent quality regressions show
5374
+ * up in diagnostics instead of looking healthier than before.
5375
+ */
5376
+ | "decompose-moved"
5333
5377
  /** There was no usable cosine lane, so results were ranked on BM25 (lexical)
5334
5378
  * alone: either the query embedding failed (or came back empty), or no
5335
5379
  * candidate had a vector to score against it because the row (re)embed failed.
@@ -8103,9 +8147,14 @@ declare function rankProfileCandidates(inputs: readonly ProfileSalienceInput[],
8103
8147
  * Unified Recall API — single retrieval surface above vault + engine.
8104
8148
  *
8105
8149
  * Budget maps to pipeline depth (mirrors Hindsight's tiered defaults):
8106
- * - `low` → V2 cosine + BM25 + recency (no rerank, no decompose)
8150
+ * - `low` → V2 cosine + BM25 + recency (no rerank)
8107
8151
  * - `mid` → V2 + cross-encoder rerank
8108
- * - `high` → V2 + rerank + LLM query decomposition for composite queries
8152
+ * - `high` → V2 + rerank + multi-hop graph traversal
8153
+ *
8154
+ * LLM-free by design (719/B4): query decomposition for composite asks
8155
+ * lives in the tool/agent layer (`createRecallTool`), which may pass
8156
+ * pre-built `subQueries` into this API. `recall()` itself never pays an
8157
+ * LLM RTT.
8109
8158
  *
8110
8159
  * Fact + chunk lanes are fused with RRF (k=60). The previous naive
8111
8160
  * "sort the union by raw score" path is gone — score scales differ
@@ -8160,7 +8209,8 @@ interface RecallToolOptions {
8160
8209
  folderId?: string | null;
8161
8210
  /** Exclude one conversation from chunk results (typically the active one). */
8162
8211
  excludeConversationId?: string;
8163
- /** LLM-decompose options; only used at budget="high". Auth follows the
8212
+ /** LLM-decompose options; only used at budget="high". Runs in THIS tool
8213
+ * executor (719/B4) — `recall()` itself is LLM-free. Auth follows the
8164
8214
  * dual pattern: apiKey (server/CLI) or getToken (browser identity
8165
8215
  * tokens) — at least one required. */
8166
8216
  decomposeOptions?: PortalLlmAuth & {
@@ -4945,15 +4945,26 @@ interface MemoryVaultSearchOptions {
4945
4945
  /** RRF smoothing constant for lane fusion. Default 60. */
4946
4946
  rrfK?: number;
4947
4947
  /**
4948
- * LLM-based query decomposition for composite/abstract queries. When set,
4949
- * each query is classified + (if composite) decomposed into 3–5 facet
4950
- * sub-queries via gpt-5-mini, then ranked via {@link rankComposite}.
4951
- * Requires `decomposeOptions` (auth) when set to "llm".
4948
+ * Pre-decomposed facet queries for the composite ranker (719/B4). When
4949
+ * ≥2 are supplied (and embeddings are available), runs {@link rankComposite}
4950
+ * over them — no LLM call inside the search path. Callers that want LLM
4951
+ * rewrite (e.g. `createRecallTool`) call `decomposeQuery` first and pass
4952
+ * the facets here.
4953
+ */
4954
+ subQueries?: string[];
4955
+ /**
4956
+ * @deprecated 719/B4 — ignored by {@link searchVaultMemories} /
4957
+ * {@link searchVaultMemoriesWithSize}. Pass {@link MemoryVaultSearchOptions.subQueries}
4958
+ * (or use `createRecallTool`). The legacy {@link createMemoryVaultSearchTool}
4959
+ * executor still honors `decompose: "llm"` + `decomposeOptions` for eval
4960
+ * parity, then forwards facets into the LLM-free search path.
4952
4961
  */
4953
4962
  decompose?: "off" | "llm";
4954
- /** Auth + endpoint for the decomposition LLM call. Required when
4955
- * decompose="llm". Auth is the dual pattern — one of `apiKey` /
4956
- * `getToken`; see {@link PortalLlmAuth}. */
4963
+ /**
4964
+ * @deprecated 719/B4 — see `decompose`. Ignored on the programmatic search
4965
+ * path; tool-layer rewrite still reads this from the search-tool options.
4966
+ * Prefer `RecallToolOptions.decomposeOptions` with `createRecallTool`.
4967
+ */
4957
4968
  decomposeOptions?: PortalLlmAuth & {
4958
4969
  baseUrl?: string;
4959
4970
  model?: string;
@@ -5118,9 +5129,11 @@ type MemoryKind = "fact" | "chunk";
5118
5129
  * Budget controls retrieval depth/cost. Higher budgets enable more
5119
5130
  * candidate sources and the cross-encoder reranker.
5120
5131
  *
5121
- * - `low`: cosine + BM25 fusion only. No reranker. Mobile default.
5122
- * - `mid`: + recency boost. Demo default.
5123
- * - `high`: + cross-encoder rerank stage. Demo `budget: high` path.
5132
+ * - `low`: cosine + BM25 + recency. No reranker. Mobile default.
5133
+ * - `mid`: + cross-encoder rerank.
5134
+ * - `high`: + multi-hop graph traversal. LLM-free — query decomposition
5135
+ * (when wanted) lives in the tool/agent layer ({@link createRecallTool}),
5136
+ * not inside `recall()` itself (719/B4).
5124
5137
  */
5125
5138
  type Budget = "low" | "mid" | "high";
5126
5139
  interface ScoreBreakdown {
@@ -5220,10 +5233,28 @@ interface RecallOptions {
5220
5233
  /** Drop results below this score. Default: 0.1 for facts, 0.5 for chunks (mirrors today's defaults). */
5221
5234
  minScore?: number;
5222
5235
  /**
5223
- * Auth + endpoint for the LLM-based query decomposition pass. Without
5224
- * these, decompose is skipped even at `budget: 'high'`. Mirrors the
5225
- * shape used by `searchVaultMemories`. Auth is the dual pattern — one
5226
- * of `apiKey` / `getToken` is required; see {@link PortalLlmAuth}.
5236
+ * Pre-decomposed facet queries for the composite ranker. When ≥2 are
5237
+ * supplied, the vault lane runs `rankComposite` over them (no LLM call
5238
+ * inside `recall()` — 719/B4). Callers that still want LLM rewrite
5239
+ * (e.g. {@link createRecallTool} at `budget: 'high'`) call
5240
+ * `decomposeQuery` themselves and pass the result here. A single entry
5241
+ * (or omitting this) keeps the single-query path.
5242
+ */
5243
+ subQueries?: string[];
5244
+ /**
5245
+ * Auth + endpoint for optional LLM helpers that reuse portal auth —
5246
+ * currently {@link RecallOptions.graphRefine} neighbor selection.
5247
+ * Query decomposition is **not** driven by this field inside `recall()`
5248
+ * (719/B4); {@link createRecallTool} reads the same shape from
5249
+ * `RecallToolOptions.decomposeOptions` for tool-layer rewrite.
5250
+ *
5251
+ * Callers that still pass `{ budget: 'high', decomposeOptions }` without
5252
+ * {@link RecallOptions.subQueries} keep compiling but no longer rewrite —
5253
+ * `recall()` emits `decompose-moved` on {@link RecallDiagnostics.degraded}
5254
+ * so upgrades without a changelog read still leave a telemetry breadcrumb.
5255
+ *
5256
+ * Auth is the dual pattern — one of `apiKey` / `getToken` is required;
5257
+ * see {@link PortalLlmAuth}.
5227
5258
  */
5228
5259
  decomposeOptions?: PortalLlmAuth & {
5229
5260
  baseUrl?: string;
@@ -5327,9 +5358,22 @@ type RecallDegradation =
5327
5358
  /** Rerank was requested (budget mid/high) but the cross-encoder didn't run
5328
5359
  * this call — unavailable (e.g. React Native) or a transient failure. */
5329
5360
  "rerank-unavailable"
5330
- /** `budget: 'high'` requested but no `decomposeOptions`, so query
5331
- * decomposition was skipped and the budget downgraded to mid. */
5361
+ /**
5362
+ * @deprecated 719/B4 moved query decomposition out of `recall()`. Budget
5363
+ * `high` no longer requires `decomposeOptions` and this signal is never
5364
+ * emitted. Prefer `"decompose-moved"` for the upgrade breadcrumb. Kept in
5365
+ * the union so existing telemetry consumers stay typed.
5366
+ */
5332
5367
  | "decompose-unavailable"
5368
+ /**
5369
+ * `budget: 'high'` with `decomposeOptions` set but no usable
5370
+ * {@link RecallOptions.subQueries}. Pre-B4 this call shape triggered LLM
5371
+ * rewrite inside `recall()`; post-B4 it stays on the single-query high
5372
+ * path (rerank + graph) and callers must pass facets (or use
5373
+ * {@link createRecallTool}). Surfaced so silent quality regressions show
5374
+ * up in diagnostics instead of looking healthier than before.
5375
+ */
5376
+ | "decompose-moved"
5333
5377
  /** There was no usable cosine lane, so results were ranked on BM25 (lexical)
5334
5378
  * alone: either the query embedding failed (or came back empty), or no
5335
5379
  * candidate had a vector to score against it because the row (re)embed failed.
@@ -8103,9 +8147,14 @@ declare function rankProfileCandidates(inputs: readonly ProfileSalienceInput[],
8103
8147
  * Unified Recall API — single retrieval surface above vault + engine.
8104
8148
  *
8105
8149
  * Budget maps to pipeline depth (mirrors Hindsight's tiered defaults):
8106
- * - `low` → V2 cosine + BM25 + recency (no rerank, no decompose)
8150
+ * - `low` → V2 cosine + BM25 + recency (no rerank)
8107
8151
  * - `mid` → V2 + cross-encoder rerank
8108
- * - `high` → V2 + rerank + LLM query decomposition for composite queries
8152
+ * - `high` → V2 + rerank + multi-hop graph traversal
8153
+ *
8154
+ * LLM-free by design (719/B4): query decomposition for composite asks
8155
+ * lives in the tool/agent layer (`createRecallTool`), which may pass
8156
+ * pre-built `subQueries` into this API. `recall()` itself never pays an
8157
+ * LLM RTT.
8109
8158
  *
8110
8159
  * Fact + chunk lanes are fused with RRF (k=60). The previous naive
8111
8160
  * "sort the union by raw score" path is gone — score scales differ
@@ -8160,7 +8209,8 @@ interface RecallToolOptions {
8160
8209
  folderId?: string | null;
8161
8210
  /** Exclude one conversation from chunk results (typically the active one). */
8162
8211
  excludeConversationId?: string;
8163
- /** LLM-decompose options; only used at budget="high". Auth follows the
8212
+ /** LLM-decompose options; only used at budget="high". Runs in THIS tool
8213
+ * executor (719/B4) — `recall()` itself is LLM-free. Auth follows the
8164
8214
  * dual pattern: apiKey (server/CLI) or getToken (browser identity
8165
8215
  * tokens) — at least one required. */
8166
8216
  decomposeOptions?: PortalLlmAuth & {
@@ -43,6 +43,7 @@ import {
43
43
  createVaultEmbeddingCache,
44
44
  createVaultMemoriesBatchOp,
45
45
  createVaultMemoryOp,
46
+ decomposeQuery,
46
47
  decryptData,
47
48
  decryptDataBatch,
48
49
  decryptDataBytes,
@@ -136,7 +137,7 @@ import {
136
137
  updateVaultMemoryOp,
137
138
  upsertMessageOp,
138
139
  vaultMemoryToStored
139
- } from "./chunk-UQDYBQVF.mjs";
140
+ } from "./chunk-FKVYUOSD.mjs";
140
141
  import {
141
142
  createStreamingDeAnonymizer,
142
143
  notifyConsolidationFallback,
@@ -6586,7 +6587,7 @@ function createRecallTool(ctx, toolOptions, callbacks) {
6586
6587
  type: "function",
6587
6588
  function: {
6588
6589
  name: RECALL_TOOL_NAME,
6589
- description: "Search the user's memory across stored facts/preferences and past conversation excerpts. Returns a unified ranked list \u2014 facts carry an `id` you can reference; conversation excerpts carry a date and role. Use this whenever the user's question may relate to anything previously discussed or saved (preferences, prior decisions, past topics). Phrase the query naturally.",
6590
+ description: `Search the user's memory across stored facts/preferences and past conversation excerpts. Returns a unified ranked list \u2014 facts carry an \`id\` you can reference; conversation excerpts carry a date and role. Use this whenever the user's question may relate to anything previously discussed or saved (preferences, prior decisions, past topics). Phrase the query naturally. For multi-faceted / overview questions ("tell me about the user", "what's my tech stack"), prefer several targeted searches \u2014 one facet each (e.g. name, work, hobbies) \u2014 over a single broad query; fuse the results yourself.`,
6590
6591
  arguments: {
6591
6592
  type: "object",
6592
6593
  properties: {
@@ -6641,6 +6642,13 @@ function createRecallTool(ctx, toolOptions, callbacks) {
6641
6642
  let surfaced = 0;
6642
6643
  try {
6643
6644
  let recallDegraded = [];
6645
+ let subQueries;
6646
+ if (defaultBudget === "high" && toolOptions?.decomposeOptions) {
6647
+ const decomp = await decomposeQuery(query, toolOptions.decomposeOptions);
6648
+ if (decomp.mode === "composite" && decomp.subQueries.length >= 2) {
6649
+ subQueries = decomp.subQueries;
6650
+ }
6651
+ }
6644
6652
  const recallOpts = {
6645
6653
  onDiagnostics: (d) => {
6646
6654
  recallDegraded = d.degraded;
@@ -6654,10 +6662,12 @@ function createRecallTool(ctx, toolOptions, callbacks) {
6654
6662
  ...toolOptions?.excludeConversationId && {
6655
6663
  excludeConversationId: toolOptions.excludeConversationId
6656
6664
  },
6657
- ...toolOptions?.decomposeOptions && {
6658
- decomposeOptions: toolOptions.decomposeOptions
6659
- },
6660
- ...toolOptions?.now !== void 0 && { now: toolOptions.now }
6665
+ // Do NOT forward decomposeOptions into recall() — rewrite already
6666
+ // ran above, and forwarding would falsely trip `decompose-moved`
6667
+ // on every specific-mode high-budget call. graphRefine auth can
6668
+ // be threaded later when the tool opts into that path.
6669
+ ...toolOptions?.now !== void 0 && { now: toolOptions.now },
6670
+ ...subQueries && { subQueries }
6661
6671
  };
6662
6672
  const result = await recall(query, ctx, recallOpts);
6663
6673
  if (callbacks?.onChunksRetrieved) {