@anuma/sdk 4.2.1 → 5.0.1

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.
@@ -6401,11 +6401,11 @@ async function decomposeQuery(query, options) {
6401
6401
  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.
6402
6402
 
6403
6403
  Query: ${trimmed}`,
6404
- // Decompose runs on the recall hot path — tighter than the
6404
+ // Decompose runs ahead of recall in the tool layer — tighter than the
6405
6405
  // portalLlm default (consolidate/extract can wait longer).
6406
6406
  timeoutMs: 2e4,
6407
6407
  // No internal retry: a failure degrades to the safe `{specific,[query]}`
6408
- // fallback below, so retrying would only add latency on the hot path.
6408
+ // fallback below. The agent/tool loop is the retry surface (719/B4).
6409
6409
  maxAttempts: 1,
6410
6410
  tag: "memory/decompose",
6411
6411
  ...options.fetchFn && { fetchFn: options.fetchFn }
@@ -6417,12 +6417,28 @@ Query: ${trimmed}`,
6417
6417
  if (parsed === null) return fallback;
6418
6418
  return validate(parsed, trimmed) ?? fallback;
6419
6419
  }
6420
+ function normalizeSubQueries(raw) {
6421
+ if (!raw || raw.length === 0) return [];
6422
+ const seen = /* @__PURE__ */ new Set();
6423
+ const out = [];
6424
+ for (const s of raw) {
6425
+ if (typeof s !== "string") continue;
6426
+ const t = s.trim();
6427
+ if (t.length === 0) continue;
6428
+ const key = t.toLowerCase();
6429
+ if (seen.has(key)) continue;
6430
+ seen.add(key);
6431
+ out.push(t);
6432
+ if (out.length >= MAX_SUB_QUERIES) break;
6433
+ }
6434
+ return out;
6435
+ }
6420
6436
  function validate(parsed, originalQuery) {
6421
6437
  if (typeof parsed !== "object" || parsed === null) return null;
6422
6438
  const obj = parsed;
6423
6439
  const mode = obj.mode === "composite" ? "composite" : "specific";
6424
6440
  if (!Array.isArray(obj.subQueries)) return null;
6425
- const subQueries = obj.subQueries.filter((s) => typeof s === "string").map((s) => s.trim()).filter((s) => s.length > 0).slice(0, MAX_SUB_QUERIES);
6441
+ const subQueries = normalizeSubQueries(obj.subQueries);
6426
6442
  if (subQueries.length === 0) return null;
6427
6443
  if (mode === "specific") {
6428
6444
  return { mode: "specific", subQueries: [originalQuery] };
@@ -7039,12 +7055,12 @@ __export(recall_exports, {
7039
7055
  function flagsForBudget(budget) {
7040
7056
  switch (budget) {
7041
7057
  case "high":
7042
- return { rerank: true, decompose: true, traverse: true };
7058
+ return { rerank: true, traverse: true };
7043
7059
  case "mid":
7044
- return { rerank: true, decompose: false, traverse: false };
7060
+ return { rerank: true, traverse: false };
7045
7061
  case "low":
7046
7062
  default:
7047
- return { rerank: false, decompose: false, traverse: false };
7063
+ return { rerank: false, traverse: false };
7048
7064
  }
7049
7065
  }
7050
7066
  function dedupeBy(items, ...keys) {
@@ -7063,10 +7079,10 @@ function dedupeBy(items, ...keys) {
7063
7079
  async function recall(query, ctx, options = {}) {
7064
7080
  const types = options.types ?? ["fact"];
7065
7081
  const limit = options.limit ?? DEFAULT_LIMIT;
7066
- const requestedBudget = options.budget ?? DEFAULT_BUDGET;
7067
- const flags = flagsForBudget(requestedBudget);
7068
- const decomposeAvailable = flags.decompose && !!options.decomposeOptions;
7069
- const usedBudget = requestedBudget === "high" && !decomposeAvailable ? "mid" : requestedBudget;
7082
+ const usedBudget = options.budget ?? DEFAULT_BUDGET;
7083
+ const flags = flagsForBudget(usedBudget);
7084
+ const normalizedFacets = normalizeSubQueries(options.subQueries);
7085
+ const subQueries = normalizedFacets.length >= 2 ? normalizedFacets : void 0;
7070
7086
  const t0 = nowMs();
7071
7087
  let prepMs = 0;
7072
7088
  let factLaneMs = 0;
@@ -7087,7 +7103,9 @@ async function recall(query, ctx, options = {}) {
7087
7103
  if (flags.rerank && factResults.length > 0 && hadV2Head && !didRerank) {
7088
7104
  degraded.push("rerank-unavailable");
7089
7105
  }
7090
- if (flags.decompose && !decomposeAvailable) degraded.push("decompose-unavailable");
7106
+ if (usedBudget === "high" && options.decomposeOptions && !subQueries && !options.graphRefine) {
7107
+ degraded.push("decompose-moved");
7108
+ }
7091
7109
  if (embeddingsUnavailable) degraded.push("embeddings-unavailable");
7092
7110
  const diagnostics = {
7093
7111
  usedBudget,
@@ -7200,10 +7218,8 @@ async function recall(query, ctx, options = {}) {
7200
7218
  },
7201
7219
  ...options.rrfK !== void 0 && { rrfK: options.rrfK },
7202
7220
  ...options.decryptLast !== void 0 && { decryptLast: options.decryptLast },
7203
- ...decomposeAvailable && {
7204
- decompose: "llm",
7205
- decomposeOptions: options.decomposeOptions
7206
- },
7221
+ // 719/B4 — composite facets arrive pre-built; no LLM call here.
7222
+ ...subQueries && { subQueries },
7207
7223
  ...options.scopes && { scopes: options.scopes },
7208
7224
  ...options.folderId !== void 0 && { folderId: options.folderId },
7209
7225
  ...options.factTypes?.length && { factTypes: options.factTypes },
@@ -7440,6 +7456,7 @@ var init_recall = __esm({
7440
7456
  init_logger();
7441
7457
  init_constants();
7442
7458
  init_embeddings();
7459
+ init_decomposeQuery();
7443
7460
  init_searchTool();
7444
7461
  init_graphTraversal();
7445
7462
  init_observationTrend();
@@ -8385,52 +8402,51 @@ async function rankPreparedVaultCandidates(query, prepared, embeddingOptions, se
8385
8402
  ...searchOptions?.rrfK !== void 0 && { rrfK: searchOptions.rrfK },
8386
8403
  ...searchOptions?.factTypeWeights && { factTypeWeights: searchOptions.factTypeWeights }
8387
8404
  };
8388
- if (useFusion && !embeddingsUnavailable && searchOptions?.decompose === "llm" && searchOptions.decomposeOptions) {
8389
- const decomp = await decomposeQuery(query, searchOptions.decomposeOptions);
8390
- if (decomp.mode === "composite") {
8391
- let subEmbeddings;
8392
- try {
8393
- subEmbeddings = await generateEmbeddings(decomp.subQueries, embeddingOptions);
8394
- } catch (err) {
8395
- getLogger().warn(
8396
- "memoryVault: sub-query embedding failed \u2014 falling back to single-query ranking: " + (err instanceof Error ? err.message : String(err))
8397
- );
8398
- subEmbeddings = [];
8399
- }
8400
- const subEmbeddingsUsable = decomp.subQueries.length > 0 && subEmbeddings.length === decomp.subQueries.length && subEmbeddings.every((v) => v.length > 0 && v.length === queryEmbedding.length);
8401
- if (subEmbeddings.length > 0 && !subEmbeddingsUsable) {
8402
- getLogger().warn(
8403
- `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`
8404
- );
8405
- }
8406
- if (subEmbeddingsUsable) {
8407
- const subQueries = decomp.subQueries.map((sq, i) => ({
8408
- query: sq,
8409
- embedding: subEmbeddings[i]
8410
- }));
8411
- const v2HeadStats = { hadResults: false };
8412
- const composite = await rankComposite(query, queryEmbedding, subQueries, embeddedItems, {
8413
- limit,
8414
- minSimilarity,
8415
- rerank: !!searchOptions.rerank,
8416
- ...searchOptions.rerankTopN !== void 0 && {
8417
- rerankTopN: searchOptions.rerankTopN
8418
- },
8419
- ...searchOptions.ceWeight !== void 0 && { ceWeight: searchOptions.ceWeight },
8420
- ...searchOptions.mmr !== void 0 && { mmr: searchOptions.mmr },
8421
- ...tuning,
8422
- ...searchOptions.entityRanking && { entityRanking: searchOptions.entityRanking },
8423
- ...searchOptions.temporalRanking && { temporalRanking: searchOptions.temporalRanking },
8424
- rerankStats,
8425
- v2HeadStats
8426
- });
8427
- return stampTimestamps({
8428
- results: composite,
8429
- vaultSize,
8430
- reranked: rerankStats.applied,
8431
- hadV2Head: v2HeadStats.hadResults
8432
- });
8433
- }
8405
+ const normalizedFacets = normalizeSubQueries(searchOptions?.subQueries);
8406
+ const facetQueries = normalizedFacets.length >= 2 ? normalizedFacets : void 0;
8407
+ if (useFusion && !embeddingsUnavailable && facetQueries) {
8408
+ let subEmbeddings;
8409
+ try {
8410
+ subEmbeddings = await generateEmbeddings(facetQueries, embeddingOptions);
8411
+ } catch (err) {
8412
+ getLogger().warn(
8413
+ "memoryVault: sub-query embedding failed \u2014 falling back to single-query ranking: " + (err instanceof Error ? err.message : String(err))
8414
+ );
8415
+ subEmbeddings = [];
8416
+ }
8417
+ const subEmbeddingsUsable = facetQueries.length > 0 && subEmbeddings.length === facetQueries.length && subEmbeddings.every((v) => v.length > 0 && v.length === queryEmbedding.length);
8418
+ if (subEmbeddings.length > 0 && !subEmbeddingsUsable) {
8419
+ getLogger().warn(
8420
+ `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`
8421
+ );
8422
+ }
8423
+ if (subEmbeddingsUsable) {
8424
+ const subQueries = facetQueries.map((sq, i) => ({
8425
+ query: sq,
8426
+ embedding: subEmbeddings[i]
8427
+ }));
8428
+ const v2HeadStats = { hadResults: false };
8429
+ const composite = await rankComposite(query, queryEmbedding, subQueries, embeddedItems, {
8430
+ limit,
8431
+ minSimilarity,
8432
+ rerank: !!searchOptions?.rerank,
8433
+ ...searchOptions?.rerankTopN !== void 0 && {
8434
+ rerankTopN: searchOptions.rerankTopN
8435
+ },
8436
+ ...searchOptions?.ceWeight !== void 0 && { ceWeight: searchOptions.ceWeight },
8437
+ ...searchOptions?.mmr !== void 0 && { mmr: searchOptions.mmr },
8438
+ ...tuning,
8439
+ ...searchOptions?.entityRanking && { entityRanking: searchOptions.entityRanking },
8440
+ ...searchOptions?.temporalRanking && { temporalRanking: searchOptions.temporalRanking },
8441
+ rerankStats,
8442
+ v2HeadStats
8443
+ });
8444
+ return stampTimestamps({
8445
+ results: composite,
8446
+ vaultSize,
8447
+ reranked: rerankStats.applied,
8448
+ hadV2Head: v2HeadStats.hadResults
8449
+ });
8434
8450
  }
8435
8451
  }
8436
8452
  if (useFusion && searchOptions?.rerank) {
@@ -8490,6 +8506,12 @@ async function searchVaultMemoriesWithSize(query, vaultCtx, embeddingOptions, ca
8490
8506
  rankedOnCosine: false
8491
8507
  };
8492
8508
  }
8509
+ if (searchOptions?.decompose === "llm" && normalizeSubQueries(searchOptions.subQueries).length < 2 && !warnedProgrammaticDecomposeIgnored) {
8510
+ warnedProgrammaticDecomposeIgnored = true;
8511
+ getLogger().warn(
8512
+ 'memoryVault: decompose:"llm" is ignored by searchVaultMemories \u2014 pass subQueries or use createMemoryVaultSearchTool / createRecallTool (719/B4)'
8513
+ );
8514
+ }
8493
8515
  const prepared = await prepareVaultCandidates(
8494
8516
  query,
8495
8517
  vaultCtx,
@@ -8594,7 +8616,8 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
8594
8616
  return "Error: A search query is required.";
8595
8617
  }
8596
8618
  try {
8597
- const budget = searchOptions?.decompose === "llm" && searchOptions.decomposeOptions ? "high" : searchOptions?.rerank ? "mid" : "low";
8619
+ const wantsDecompose = searchOptions?.decompose === "llm" && !!searchOptions.decomposeOptions;
8620
+ const budget = wantsDecompose ? "high" : searchOptions?.rerank ? "mid" : "low";
8598
8621
  const folderId = searchOptions?.folderId ?? argFolderId;
8599
8622
  if (searchOptions?.useFusion === false) {
8600
8623
  const { results: legacy, embeddingsUnavailable } = await searchVaultMemoriesWithSize(
@@ -8618,6 +8641,13 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
8618
8641
  legacy.map((r) => ({ id: r.uniqueId, content: r.content, score: r.similarity }))
8619
8642
  );
8620
8643
  }
8644
+ let subQueries;
8645
+ if (wantsDecompose && searchOptions?.decomposeOptions) {
8646
+ const decomp = await decomposeQuery(query, searchOptions.decomposeOptions);
8647
+ if (decomp.mode === "composite" && decomp.subQueries.length >= 2) {
8648
+ subQueries = decomp.subQueries;
8649
+ }
8650
+ }
8621
8651
  const { recall: recall2 } = await Promise.resolve().then(() => (init_recall(), recall_exports));
8622
8652
  let recallDegraded = [];
8623
8653
  const result = await recall2(
@@ -8634,9 +8664,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
8634
8664
  ...tuningForward,
8635
8665
  ...folderId !== void 0 && { folderId },
8636
8666
  ...searchOptions?.scopes && { scopes: searchOptions.scopes },
8637
- ...searchOptions?.decompose === "llm" && searchOptions.decomposeOptions && {
8638
- decomposeOptions: searchOptions.decomposeOptions
8639
- }
8667
+ ...subQueries && { subQueries }
8640
8668
  }
8641
8669
  );
8642
8670
  if (result.vaultSize === 0) {
@@ -8666,7 +8694,7 @@ ${formatted}`;
8666
8694
  }
8667
8695
  };
8668
8696
  }
8669
- var SUPERSESSION_SIMILARITY_THRESHOLD, SUPERSESSION_MIN_AGE_GAP_MS, SUPERSESSION_BOOST_FACTOR, SUPERSESSION_MAX_WINDOW, EMBEDDINGS_DEGRADED_EMPTY_NO_LEXICAL;
8697
+ var warnedProgrammaticDecomposeIgnored, SUPERSESSION_SIMILARITY_THRESHOLD, SUPERSESSION_MIN_AGE_GAP_MS, SUPERSESSION_BOOST_FACTOR, SUPERSESSION_MAX_WINDOW, EMBEDDINGS_DEGRADED_EMPTY_NO_LEXICAL;
8670
8698
  var init_searchTool = __esm({
8671
8699
  "src/lib/memoryVault/searchTool.ts"() {
8672
8700
  "use strict";
@@ -8684,6 +8712,7 @@ var init_searchTool = __esm({
8684
8712
  init_bm25();
8685
8713
  init_decomposeQuery();
8686
8714
  init_lruCache();
8715
+ warnedProgrammaticDecomposeIgnored = false;
8687
8716
  SUPERSESSION_SIMILARITY_THRESHOLD = 0.7;
8688
8717
  SUPERSESSION_MIN_AGE_GAP_MS = 30 * 24 * 60 * 60 * 1e3;
8689
8718
  SUPERSESSION_BOOST_FACTOR = 0.8;
@@ -16715,6 +16744,7 @@ function rankProfileCandidates(inputs, now = Date.now(), options = {}) {
16715
16744
  init_recall();
16716
16745
 
16717
16746
  // src/lib/memory/recallTool.ts
16747
+ init_decomposeQuery();
16718
16748
  init_recall();
16719
16749
  init_recallConstants();
16720
16750
  var DEFAULT_LIMIT2 = 8;
@@ -16812,7 +16842,7 @@ function createRecallTool(ctx, toolOptions, callbacks) {
16812
16842
  type: "function",
16813
16843
  function: {
16814
16844
  name: RECALL_TOOL_NAME,
16815
- 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.",
16845
+ 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.`,
16816
16846
  arguments: {
16817
16847
  type: "object",
16818
16848
  properties: {
@@ -16867,6 +16897,13 @@ function createRecallTool(ctx, toolOptions, callbacks) {
16867
16897
  let surfaced = 0;
16868
16898
  try {
16869
16899
  let recallDegraded = [];
16900
+ let subQueries;
16901
+ if (defaultBudget === "high" && toolOptions?.decomposeOptions) {
16902
+ const decomp = await decomposeQuery(query, toolOptions.decomposeOptions);
16903
+ if (decomp.mode === "composite" && decomp.subQueries.length >= 2) {
16904
+ subQueries = decomp.subQueries;
16905
+ }
16906
+ }
16870
16907
  const recallOpts = {
16871
16908
  onDiagnostics: (d) => {
16872
16909
  recallDegraded = d.degraded;
@@ -16880,10 +16917,12 @@ function createRecallTool(ctx, toolOptions, callbacks) {
16880
16917
  ...toolOptions?.excludeConversationId && {
16881
16918
  excludeConversationId: toolOptions.excludeConversationId
16882
16919
  },
16883
- ...toolOptions?.decomposeOptions && {
16884
- decomposeOptions: toolOptions.decomposeOptions
16885
- },
16886
- ...toolOptions?.now !== void 0 && { now: toolOptions.now }
16920
+ // Do NOT forward decomposeOptions into recall() — rewrite already
16921
+ // ran above, and forwarding would falsely trip `decompose-moved`
16922
+ // on every specific-mode high-budget call. graphRefine auth can
16923
+ // be threaded later when the tool opts into that path.
16924
+ ...toolOptions?.now !== void 0 && { now: toolOptions.now },
16925
+ ...subQueries && { subQueries }
16887
16926
  };
16888
16927
  const result = await recall(query, ctx, recallOpts);
16889
16928
  if (callbacks?.onChunksRetrieved) {
@@ -21017,7 +21056,7 @@ __decorateClass([
21017
21056
  ], VaultFolder.prototype, "context", 2);
21018
21057
 
21019
21058
  // src/lib/db/schema.ts
21020
- var SDK_SCHEMA_VERSION = 42;
21059
+ var SDK_SCHEMA_VERSION = 43;
21021
21060
  var sdkSchema = (0, import_watermelondb19.appSchema)({
21022
21061
  version: SDK_SCHEMA_VERSION,
21023
21062
  tables: [
@@ -21065,7 +21104,16 @@ var sdkSchema = (0, import_watermelondb19.appSchema)({
21065
21104
  { name: "conversation_id", type: "string", isIndexed: true },
21066
21105
  { name: "title", type: "string" },
21067
21106
  { name: "project_id", type: "string", isOptional: true, isIndexed: true },
21068
- { name: "created_at", type: "number" },
21107
+ // Indexed to match every other list-sorted created_at in this schema.
21108
+ // Note this single-column index is NOT what makes the conversation list
21109
+ // reads fast on native SQLite — they all filter is_deleted as well, and
21110
+ // SQLite will not combine the two indexes, so the composite created by
21111
+ // the v42 migration is what its planner actually uses. This declaration
21112
+ // still earns its keep on the other two adapters: LokiJS builds its
21113
+ // binary indices from `isIndexed` (and ignores `sql` migration steps
21114
+ // outright), and Postgres gathers statistics via autovacuum, so its
21115
+ // planner can use a single-column index the SQLite planner skips.
21116
+ { name: "created_at", type: "number", isIndexed: true },
21069
21117
  { name: "updated_at", type: "number" },
21070
21118
  { name: "is_deleted", type: "boolean", isIndexed: true },
21071
21119
  { name: "pinned_at", type: "number", isOptional: true }
@@ -21932,6 +21980,49 @@ var sdkMigrations = (0, import_migrations2.schemaMigrations)({
21932
21980
  ]
21933
21981
  })
21934
21982
  ]
21983
+ },
21984
+ // v42 -> v43: Give the conversation list reads an index they can actually
21985
+ // sort on. All five of them (getConversationsOp, getConversationsLazyOp,
21986
+ // getConversationsByProjectOp, getConversationsByProjectLazyOp and the
21987
+ // keyset getConversationsPageOp) emit `where is_deleted is 0 ... order by
21988
+ // created_at desc`, so before this they resolved is_deleted through its
21989
+ // index and then built a temp B-tree over every live row just to return
21990
+ // the newest page.
21991
+ //
21992
+ // The index is COMPOSITE, and that is the whole point — a bare index on
21993
+ // created_at does not fix this. SQLite will not combine two single-column
21994
+ // indexes here, so with `is_deleted` indexed and `created_at` indexed
21995
+ // separately it still picks conversations_is_deleted and still temp-sorts.
21996
+ // It only prefers a lone created_at index once ANALYZE has populated
21997
+ // sqlite_stat1, and nothing ever runs ANALYZE: not WatermelonDB, not its
21998
+ // native SQLite bindings, not this SDK. Every device is permanently in the
21999
+ // no-statistics state, so the index has to satisfy the filter and the sort
22000
+ // in one structure. `(is_deleted, created_at)` does: equality on the
22001
+ // leading column, ordered scan on the second, and the keyset boundary
22002
+ // becomes a range seek on the same index.
22003
+ //
22004
+ // unsafeExecuteSql, not addColumns: both columns already exist and
22005
+ // WatermelonDB has no add-index migration step, so raw SQL is the only way
22006
+ // to build one in place (same shape as the v19 -> v20 memory_vault
22007
+ // migration). IF NOT EXISTS keeps the step idempotent.
22008
+ //
22009
+ // Two limits worth knowing before someone re-measures this and finds it
22010
+ // missing. First, the LokiJS (web) adapter discards `sql` steps entirely,
22011
+ // so existing browser databases gain nothing — web's only lever is the
22012
+ // `isIndexed` flag on the column itself. Second, this index reaches
22013
+ // MIGRATED databases only: WatermelonDB builds a fresh database purely from
22014
+ // the encoded schema, and its schema format cannot express a composite
22015
+ // index. The one hook that could (`unsafeSql` on the table) is a function,
22016
+ // and the LokiJS adapter posts the schema to its worker, so attaching it
22017
+ // makes the whole schema fail structuredClone and takes the database down
22018
+ // at setup for anyone on `useWebWorker: true`. Not worth it for an index.
22019
+ {
22020
+ toVersion: 43,
22021
+ steps: [
22022
+ (0, import_migrations2.unsafeExecuteSql)(
22023
+ "CREATE INDEX IF NOT EXISTS conversations_is_deleted_created_at ON conversations (is_deleted, created_at);"
22024
+ )
22025
+ ]
21935
22026
  }
21936
22027
  ]
21937
22028
  });
@@ -5009,15 +5009,26 @@ interface MemoryVaultSearchOptions {
5009
5009
  /** RRF smoothing constant for lane fusion. Default 60. */
5010
5010
  rrfK?: number;
5011
5011
  /**
5012
- * LLM-based query decomposition for composite/abstract queries. When set,
5013
- * each query is classified + (if composite) decomposed into 3–5 facet
5014
- * sub-queries via gpt-5-mini, then ranked via {@link rankComposite}.
5015
- * Requires `decomposeOptions` (auth) when set to "llm".
5012
+ * Pre-decomposed facet queries for the composite ranker (719/B4). When
5013
+ * ≥2 are supplied (and embeddings are available), runs {@link rankComposite}
5014
+ * over them — no LLM call inside the search path. Callers that want LLM
5015
+ * rewrite (e.g. `createRecallTool`) call `decomposeQuery` first and pass
5016
+ * the facets here.
5017
+ */
5018
+ subQueries?: string[];
5019
+ /**
5020
+ * @deprecated 719/B4 — ignored by {@link searchVaultMemories} /
5021
+ * {@link searchVaultMemoriesWithSize}. Pass {@link MemoryVaultSearchOptions.subQueries}
5022
+ * (or use `createRecallTool`). The legacy {@link createMemoryVaultSearchTool}
5023
+ * executor still honors `decompose: "llm"` + `decomposeOptions` for eval
5024
+ * parity, then forwards facets into the LLM-free search path.
5016
5025
  */
5017
5026
  decompose?: "off" | "llm";
5018
- /** Auth + endpoint for the decomposition LLM call. Required when
5019
- * decompose="llm". Auth is the dual pattern — one of `apiKey` /
5020
- * `getToken`; see {@link PortalLlmAuth}. */
5027
+ /**
5028
+ * @deprecated 719/B4 — see `decompose`. Ignored on the programmatic search
5029
+ * path; tool-layer rewrite still reads this from the search-tool options.
5030
+ * Prefer `RecallToolOptions.decomposeOptions` with `createRecallTool`.
5031
+ */
5021
5032
  decomposeOptions?: PortalLlmAuth & {
5022
5033
  baseUrl?: string;
5023
5034
  model?: string;
@@ -5182,9 +5193,11 @@ type MemoryKind = "fact" | "chunk";
5182
5193
  * Budget controls retrieval depth/cost. Higher budgets enable more
5183
5194
  * candidate sources and the cross-encoder reranker.
5184
5195
  *
5185
- * - `low`: cosine + BM25 fusion only. No reranker. Mobile default.
5186
- * - `mid`: + recency boost. Demo default.
5187
- * - `high`: + cross-encoder rerank stage. Demo `budget: high` path.
5196
+ * - `low`: cosine + BM25 + recency. No reranker. Mobile default.
5197
+ * - `mid`: + cross-encoder rerank.
5198
+ * - `high`: + multi-hop graph traversal. LLM-free — query decomposition
5199
+ * (when wanted) lives in the tool/agent layer ({@link createRecallTool}),
5200
+ * not inside `recall()` itself (719/B4).
5188
5201
  */
5189
5202
  type Budget = "low" | "mid" | "high";
5190
5203
  interface ScoreBreakdown {
@@ -5284,10 +5297,28 @@ interface RecallOptions {
5284
5297
  /** Drop results below this score. Default: 0.1 for facts, 0.5 for chunks (mirrors today's defaults). */
5285
5298
  minScore?: number;
5286
5299
  /**
5287
- * Auth + endpoint for the LLM-based query decomposition pass. Without
5288
- * these, decompose is skipped even at `budget: 'high'`. Mirrors the
5289
- * shape used by `searchVaultMemories`. Auth is the dual pattern — one
5290
- * of `apiKey` / `getToken` is required; see {@link PortalLlmAuth}.
5300
+ * Pre-decomposed facet queries for the composite ranker. When ≥2 are
5301
+ * supplied, the vault lane runs `rankComposite` over them (no LLM call
5302
+ * inside `recall()` — 719/B4). Callers that still want LLM rewrite
5303
+ * (e.g. {@link createRecallTool} at `budget: 'high'`) call
5304
+ * `decomposeQuery` themselves and pass the result here. A single entry
5305
+ * (or omitting this) keeps the single-query path.
5306
+ */
5307
+ subQueries?: string[];
5308
+ /**
5309
+ * Auth + endpoint for optional LLM helpers that reuse portal auth —
5310
+ * currently {@link RecallOptions.graphRefine} neighbor selection.
5311
+ * Query decomposition is **not** driven by this field inside `recall()`
5312
+ * (719/B4); {@link createRecallTool} reads the same shape from
5313
+ * `RecallToolOptions.decomposeOptions` for tool-layer rewrite.
5314
+ *
5315
+ * Callers that still pass `{ budget: 'high', decomposeOptions }` without
5316
+ * {@link RecallOptions.subQueries} keep compiling but no longer rewrite —
5317
+ * `recall()` emits `decompose-moved` on {@link RecallDiagnostics.degraded}
5318
+ * so upgrades without a changelog read still leave a telemetry breadcrumb.
5319
+ *
5320
+ * Auth is the dual pattern — one of `apiKey` / `getToken` is required;
5321
+ * see {@link PortalLlmAuth}.
5291
5322
  */
5292
5323
  decomposeOptions?: PortalLlmAuth & {
5293
5324
  baseUrl?: string;
@@ -5391,9 +5422,22 @@ type RecallDegradation =
5391
5422
  /** Rerank was requested (budget mid/high) but the cross-encoder didn't run
5392
5423
  * this call — unavailable (e.g. React Native) or a transient failure. */
5393
5424
  "rerank-unavailable"
5394
- /** `budget: 'high'` requested but no `decomposeOptions`, so query
5395
- * decomposition was skipped and the budget downgraded to mid. */
5425
+ /**
5426
+ * @deprecated 719/B4 moved query decomposition out of `recall()`. Budget
5427
+ * `high` no longer requires `decomposeOptions` and this signal is never
5428
+ * emitted. Prefer `"decompose-moved"` for the upgrade breadcrumb. Kept in
5429
+ * the union so existing telemetry consumers stay typed.
5430
+ */
5396
5431
  | "decompose-unavailable"
5432
+ /**
5433
+ * `budget: 'high'` with `decomposeOptions` set but no usable
5434
+ * {@link RecallOptions.subQueries}. Pre-B4 this call shape triggered LLM
5435
+ * rewrite inside `recall()`; post-B4 it stays on the single-query high
5436
+ * path (rerank + graph) and callers must pass facets (or use
5437
+ * {@link createRecallTool}). Surfaced so silent quality regressions show
5438
+ * up in diagnostics instead of looking healthier than before.
5439
+ */
5440
+ | "decompose-moved"
5397
5441
  /** There was no usable cosine lane, so results were ranked on BM25 (lexical)
5398
5442
  * alone: either the query embedding failed (or came back empty), or no
5399
5443
  * candidate had a vector to score against it because the row (re)embed failed.
@@ -7463,9 +7507,14 @@ declare function rankProfileCandidates(inputs: readonly ProfileSalienceInput[],
7463
7507
  * Unified Recall API — single retrieval surface above vault + engine.
7464
7508
  *
7465
7509
  * Budget maps to pipeline depth (mirrors Hindsight's tiered defaults):
7466
- * - `low` → V2 cosine + BM25 + recency (no rerank, no decompose)
7510
+ * - `low` → V2 cosine + BM25 + recency (no rerank)
7467
7511
  * - `mid` → V2 + cross-encoder rerank
7468
- * - `high` → V2 + rerank + LLM query decomposition for composite queries
7512
+ * - `high` → V2 + rerank + multi-hop graph traversal
7513
+ *
7514
+ * LLM-free by design (719/B4): query decomposition for composite asks
7515
+ * lives in the tool/agent layer (`createRecallTool`), which may pass
7516
+ * pre-built `subQueries` into this API. `recall()` itself never pays an
7517
+ * LLM RTT.
7469
7518
  *
7470
7519
  * Fact + chunk lanes are fused with RRF (k=60). The previous naive
7471
7520
  * "sort the union by raw score" path is gone — score scales differ
@@ -7520,7 +7569,8 @@ interface RecallToolOptions {
7520
7569
  folderId?: string | null;
7521
7570
  /** Exclude one conversation from chunk results (typically the active one). */
7522
7571
  excludeConversationId?: string;
7523
- /** LLM-decompose options; only used at budget="high". Auth follows the
7572
+ /** LLM-decompose options; only used at budget="high". Runs in THIS tool
7573
+ * executor (719/B4) — `recall()` itself is LLM-free. Auth follows the
7524
7574
  * dual pattern: apiKey (server/CLI) or getToken (browser identity
7525
7575
  * tokens) — at least one required. */
7526
7576
  decomposeOptions?: PortalLlmAuth & {
@@ -8729,8 +8779,12 @@ declare function useChatStorage(options: UseChatStorageOptions): UseChatStorageR
8729
8779
  * `topics_updated_at` is a SECOND timestamp because every topic writer pins
8730
8780
  * `updated_at` on purpose (recall's recency multiplier) and both sync paths
8731
8781
  * key on `updated_at`, so a topic-only change would neither upload nor merge
8782
+ * - v43: Added a (is_deleted, created_at) index to conversations. Every
8783
+ * conversation list read filters is_deleted and orders by created_at DESC,
8784
+ * which previously meant a temp B-tree sort of the whole live set on every
8785
+ * read. Structural only — no column added, no data rewritten
8732
8786
  */
8733
- declare const SDK_SCHEMA_VERSION = 42;
8787
+ declare const SDK_SCHEMA_VERSION = 43;
8734
8788
  /**
8735
8789
  * Combined WatermelonDB schema for all SDK storage modules.
8736
8790
  *
@@ -8817,6 +8871,7 @@ declare const sdkSchema: Readonly<{
8817
8871
  * - v39 → v40: Added `fact_type`, `archived_at`, `trust_tier` columns to memory_vault for typed memory + decay + Tier-0 security (all nullable + plaintext, NULL backfill)
8818
8872
  * - v40 → v41: Added `visibility`, `twin_opt_in`, `published_at`, `geohash` columns to memory_vault for the People Nearby cross-user visibility axis (two-tier `private | public`; null/unknown grandfathered as 'private')
8819
8873
  * - v41 → v42: Added `topics` + `topics_updated_at` columns to memory_vault, making a memory's topics the durable synced record and `entity`/`memory_entity` a device-local index over it (null `topics` = pre-v42, backfilled from the row's current links by the sweep)
8874
+ * - v42 → v43: Added a composite `(is_deleted, created_at)` index to conversations so the list reads stop temp-sorting (structural only, no data rewritten)
8820
8875
  */
8821
8876
  declare const sdkMigrations: Readonly<{
8822
8877
  validated: true;