@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.
@@ -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;
@@ -24496,7 +24525,7 @@ __decorateClass([
24496
24525
  ], VaultFolder.prototype, "context", 2);
24497
24526
 
24498
24527
  // src/lib/db/schema.ts
24499
- var SDK_SCHEMA_VERSION = 42;
24528
+ var SDK_SCHEMA_VERSION = 43;
24500
24529
  var sdkSchema = (0, import_watermelondb12.appSchema)({
24501
24530
  version: SDK_SCHEMA_VERSION,
24502
24531
  tables: [
@@ -24544,7 +24573,16 @@ var sdkSchema = (0, import_watermelondb12.appSchema)({
24544
24573
  { name: "conversation_id", type: "string", isIndexed: true },
24545
24574
  { name: "title", type: "string" },
24546
24575
  { name: "project_id", type: "string", isOptional: true, isIndexed: true },
24547
- { name: "created_at", type: "number" },
24576
+ // Indexed to match every other list-sorted created_at in this schema.
24577
+ // Note this single-column index is NOT what makes the conversation list
24578
+ // reads fast on native SQLite — they all filter is_deleted as well, and
24579
+ // SQLite will not combine the two indexes, so the composite created by
24580
+ // the v42 migration is what its planner actually uses. This declaration
24581
+ // still earns its keep on the other two adapters: LokiJS builds its
24582
+ // binary indices from `isIndexed` (and ignores `sql` migration steps
24583
+ // outright), and Postgres gathers statistics via autovacuum, so its
24584
+ // planner can use a single-column index the SQLite planner skips.
24585
+ { name: "created_at", type: "number", isIndexed: true },
24548
24586
  { name: "updated_at", type: "number" },
24549
24587
  { name: "is_deleted", type: "boolean", isIndexed: true },
24550
24588
  { name: "pinned_at", type: "number", isOptional: true }
@@ -25411,6 +25449,49 @@ var sdkMigrations = (0, import_migrations.schemaMigrations)({
25411
25449
  ]
25412
25450
  })
25413
25451
  ]
25452
+ },
25453
+ // v42 -> v43: Give the conversation list reads an index they can actually
25454
+ // sort on. All five of them (getConversationsOp, getConversationsLazyOp,
25455
+ // getConversationsByProjectOp, getConversationsByProjectLazyOp and the
25456
+ // keyset getConversationsPageOp) emit `where is_deleted is 0 ... order by
25457
+ // created_at desc`, so before this they resolved is_deleted through its
25458
+ // index and then built a temp B-tree over every live row just to return
25459
+ // the newest page.
25460
+ //
25461
+ // The index is COMPOSITE, and that is the whole point — a bare index on
25462
+ // created_at does not fix this. SQLite will not combine two single-column
25463
+ // indexes here, so with `is_deleted` indexed and `created_at` indexed
25464
+ // separately it still picks conversations_is_deleted and still temp-sorts.
25465
+ // It only prefers a lone created_at index once ANALYZE has populated
25466
+ // sqlite_stat1, and nothing ever runs ANALYZE: not WatermelonDB, not its
25467
+ // native SQLite bindings, not this SDK. Every device is permanently in the
25468
+ // no-statistics state, so the index has to satisfy the filter and the sort
25469
+ // in one structure. `(is_deleted, created_at)` does: equality on the
25470
+ // leading column, ordered scan on the second, and the keyset boundary
25471
+ // becomes a range seek on the same index.
25472
+ //
25473
+ // unsafeExecuteSql, not addColumns: both columns already exist and
25474
+ // WatermelonDB has no add-index migration step, so raw SQL is the only way
25475
+ // to build one in place (same shape as the v19 -> v20 memory_vault
25476
+ // migration). IF NOT EXISTS keeps the step idempotent.
25477
+ //
25478
+ // Two limits worth knowing before someone re-measures this and finds it
25479
+ // missing. First, the LokiJS (web) adapter discards `sql` steps entirely,
25480
+ // so existing browser databases gain nothing — web's only lever is the
25481
+ // `isIndexed` flag on the column itself. Second, this index reaches
25482
+ // MIGRATED databases only: WatermelonDB builds a fresh database purely from
25483
+ // the encoded schema, and its schema format cannot express a composite
25484
+ // index. The one hook that could (`unsafeSql` on the table) is a function,
25485
+ // and the LokiJS adapter posts the schema to its worker, so attaching it
25486
+ // makes the whole schema fail structuredClone and takes the database down
25487
+ // at setup for anyone on `useWebWorker: true`. Not worth it for an index.
25488
+ {
25489
+ toVersion: 43,
25490
+ steps: [
25491
+ (0, import_migrations.unsafeExecuteSql)(
25492
+ "CREATE INDEX IF NOT EXISTS conversations_is_deleted_created_at ON conversations (is_deleted, created_at);"
25493
+ )
25494
+ ]
25414
25495
  }
25415
25496
  ]
25416
25497
  });
@@ -29733,6 +29814,7 @@ function rankProfileCandidates(inputs, now = Date.now(), options = {}) {
29733
29814
  init_recall();
29734
29815
 
29735
29816
  // src/lib/memory/recallTool.ts
29817
+ init_decomposeQuery();
29736
29818
  init_recall();
29737
29819
  init_recallConstants();
29738
29820
  var DEFAULT_LIMIT2 = 8;
@@ -29830,7 +29912,7 @@ function createRecallTool(ctx, toolOptions, callbacks) {
29830
29912
  type: "function",
29831
29913
  function: {
29832
29914
  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.",
29915
+ 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
29916
  arguments: {
29835
29917
  type: "object",
29836
29918
  properties: {
@@ -29885,6 +29967,13 @@ function createRecallTool(ctx, toolOptions, callbacks) {
29885
29967
  let surfaced = 0;
29886
29968
  try {
29887
29969
  let recallDegraded = [];
29970
+ let subQueries;
29971
+ if (defaultBudget === "high" && toolOptions?.decomposeOptions) {
29972
+ const decomp = await decomposeQuery(query, toolOptions.decomposeOptions);
29973
+ if (decomp.mode === "composite" && decomp.subQueries.length >= 2) {
29974
+ subQueries = decomp.subQueries;
29975
+ }
29976
+ }
29888
29977
  const recallOpts = {
29889
29978
  onDiagnostics: (d) => {
29890
29979
  recallDegraded = d.degraded;
@@ -29898,10 +29987,12 @@ function createRecallTool(ctx, toolOptions, callbacks) {
29898
29987
  ...toolOptions?.excludeConversationId && {
29899
29988
  excludeConversationId: toolOptions.excludeConversationId
29900
29989
  },
29901
- ...toolOptions?.decomposeOptions && {
29902
- decomposeOptions: toolOptions.decomposeOptions
29903
- },
29904
- ...toolOptions?.now !== void 0 && { now: toolOptions.now }
29990
+ // Do NOT forward decomposeOptions into recall() — rewrite already
29991
+ // ran above, and forwarding would falsely trip `decompose-moved`
29992
+ // on every specific-mode high-budget call. graphRefine auth can
29993
+ // be threaded later when the tool opts into that path.
29994
+ ...toolOptions?.now !== void 0 && { now: toolOptions.now },
29995
+ ...subQueries && { subQueries }
29905
29996
  };
29906
29997
  const result = await recall(query, ctx, recallOpts);
29907
29998
  if (callbacks?.onChunksRetrieved) {
@@ -87,8 +87,12 @@ import { ResultCallback } from '@nozbe/watermelondb/utils/fp/Result';
87
87
  * `topics_updated_at` is a SECOND timestamp because every topic writer pins
88
88
  * `updated_at` on purpose (recall's recency multiplier) and both sync paths
89
89
  * key on `updated_at`, so a topic-only change would neither upload nor merge
90
+ * - v43: Added a (is_deleted, created_at) index to conversations. Every
91
+ * conversation list read filters is_deleted and orders by created_at DESC,
92
+ * which previously meant a temp B-tree sort of the whole live set on every
93
+ * read. Structural only — no column added, no data rewritten
90
94
  */
91
- declare const SDK_SCHEMA_VERSION = 42;
95
+ declare const SDK_SCHEMA_VERSION = 43;
92
96
  /**
93
97
  * Combined WatermelonDB schema for all SDK storage modules.
94
98
  *
@@ -175,6 +179,7 @@ declare const sdkSchema: Readonly<{
175
179
  * - 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)
176
180
  * - 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')
177
181
  * - 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)
182
+ * - v42 → v43: Added a composite `(is_deleted, created_at)` index to conversations so the list reads stop temp-sorting (structural only, no data rewritten)
178
183
  */
179
184
  declare const sdkMigrations: Readonly<{
180
185
  validated: true;
@@ -4945,15 +4950,26 @@ interface MemoryVaultSearchOptions {
4945
4950
  /** RRF smoothing constant for lane fusion. Default 60. */
4946
4951
  rrfK?: number;
4947
4952
  /**
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".
4953
+ * Pre-decomposed facet queries for the composite ranker (719/B4). When
4954
+ * ≥2 are supplied (and embeddings are available), runs {@link rankComposite}
4955
+ * over them — no LLM call inside the search path. Callers that want LLM
4956
+ * rewrite (e.g. `createRecallTool`) call `decomposeQuery` first and pass
4957
+ * the facets here.
4958
+ */
4959
+ subQueries?: string[];
4960
+ /**
4961
+ * @deprecated 719/B4 — ignored by {@link searchVaultMemories} /
4962
+ * {@link searchVaultMemoriesWithSize}. Pass {@link MemoryVaultSearchOptions.subQueries}
4963
+ * (or use `createRecallTool`). The legacy {@link createMemoryVaultSearchTool}
4964
+ * executor still honors `decompose: "llm"` + `decomposeOptions` for eval
4965
+ * parity, then forwards facets into the LLM-free search path.
4952
4966
  */
4953
4967
  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}. */
4968
+ /**
4969
+ * @deprecated 719/B4 — see `decompose`. Ignored on the programmatic search
4970
+ * path; tool-layer rewrite still reads this from the search-tool options.
4971
+ * Prefer `RecallToolOptions.decomposeOptions` with `createRecallTool`.
4972
+ */
4957
4973
  decomposeOptions?: PortalLlmAuth & {
4958
4974
  baseUrl?: string;
4959
4975
  model?: string;
@@ -5118,9 +5134,11 @@ type MemoryKind = "fact" | "chunk";
5118
5134
  * Budget controls retrieval depth/cost. Higher budgets enable more
5119
5135
  * candidate sources and the cross-encoder reranker.
5120
5136
  *
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.
5137
+ * - `low`: cosine + BM25 + recency. No reranker. Mobile default.
5138
+ * - `mid`: + cross-encoder rerank.
5139
+ * - `high`: + multi-hop graph traversal. LLM-free — query decomposition
5140
+ * (when wanted) lives in the tool/agent layer ({@link createRecallTool}),
5141
+ * not inside `recall()` itself (719/B4).
5124
5142
  */
5125
5143
  type Budget = "low" | "mid" | "high";
5126
5144
  interface ScoreBreakdown {
@@ -5220,10 +5238,28 @@ interface RecallOptions {
5220
5238
  /** Drop results below this score. Default: 0.1 for facts, 0.5 for chunks (mirrors today's defaults). */
5221
5239
  minScore?: number;
5222
5240
  /**
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}.
5241
+ * Pre-decomposed facet queries for the composite ranker. When ≥2 are
5242
+ * supplied, the vault lane runs `rankComposite` over them (no LLM call
5243
+ * inside `recall()` — 719/B4). Callers that still want LLM rewrite
5244
+ * (e.g. {@link createRecallTool} at `budget: 'high'`) call
5245
+ * `decomposeQuery` themselves and pass the result here. A single entry
5246
+ * (or omitting this) keeps the single-query path.
5247
+ */
5248
+ subQueries?: string[];
5249
+ /**
5250
+ * Auth + endpoint for optional LLM helpers that reuse portal auth —
5251
+ * currently {@link RecallOptions.graphRefine} neighbor selection.
5252
+ * Query decomposition is **not** driven by this field inside `recall()`
5253
+ * (719/B4); {@link createRecallTool} reads the same shape from
5254
+ * `RecallToolOptions.decomposeOptions` for tool-layer rewrite.
5255
+ *
5256
+ * Callers that still pass `{ budget: 'high', decomposeOptions }` without
5257
+ * {@link RecallOptions.subQueries} keep compiling but no longer rewrite —
5258
+ * `recall()` emits `decompose-moved` on {@link RecallDiagnostics.degraded}
5259
+ * so upgrades without a changelog read still leave a telemetry breadcrumb.
5260
+ *
5261
+ * Auth is the dual pattern — one of `apiKey` / `getToken` is required;
5262
+ * see {@link PortalLlmAuth}.
5227
5263
  */
5228
5264
  decomposeOptions?: PortalLlmAuth & {
5229
5265
  baseUrl?: string;
@@ -5327,9 +5363,22 @@ type RecallDegradation =
5327
5363
  /** Rerank was requested (budget mid/high) but the cross-encoder didn't run
5328
5364
  * this call — unavailable (e.g. React Native) or a transient failure. */
5329
5365
  "rerank-unavailable"
5330
- /** `budget: 'high'` requested but no `decomposeOptions`, so query
5331
- * decomposition was skipped and the budget downgraded to mid. */
5366
+ /**
5367
+ * @deprecated 719/B4 moved query decomposition out of `recall()`. Budget
5368
+ * `high` no longer requires `decomposeOptions` and this signal is never
5369
+ * emitted. Prefer `"decompose-moved"` for the upgrade breadcrumb. Kept in
5370
+ * the union so existing telemetry consumers stay typed.
5371
+ */
5332
5372
  | "decompose-unavailable"
5373
+ /**
5374
+ * `budget: 'high'` with `decomposeOptions` set but no usable
5375
+ * {@link RecallOptions.subQueries}. Pre-B4 this call shape triggered LLM
5376
+ * rewrite inside `recall()`; post-B4 it stays on the single-query high
5377
+ * path (rerank + graph) and callers must pass facets (or use
5378
+ * {@link createRecallTool}). Surfaced so silent quality regressions show
5379
+ * up in diagnostics instead of looking healthier than before.
5380
+ */
5381
+ | "decompose-moved"
5333
5382
  /** There was no usable cosine lane, so results were ranked on BM25 (lexical)
5334
5383
  * alone: either the query embedding failed (or came back empty), or no
5335
5384
  * candidate had a vector to score against it because the row (re)embed failed.
@@ -8103,9 +8152,14 @@ declare function rankProfileCandidates(inputs: readonly ProfileSalienceInput[],
8103
8152
  * Unified Recall API — single retrieval surface above vault + engine.
8104
8153
  *
8105
8154
  * Budget maps to pipeline depth (mirrors Hindsight's tiered defaults):
8106
- * - `low` → V2 cosine + BM25 + recency (no rerank, no decompose)
8155
+ * - `low` → V2 cosine + BM25 + recency (no rerank)
8107
8156
  * - `mid` → V2 + cross-encoder rerank
8108
- * - `high` → V2 + rerank + LLM query decomposition for composite queries
8157
+ * - `high` → V2 + rerank + multi-hop graph traversal
8158
+ *
8159
+ * LLM-free by design (719/B4): query decomposition for composite asks
8160
+ * lives in the tool/agent layer (`createRecallTool`), which may pass
8161
+ * pre-built `subQueries` into this API. `recall()` itself never pays an
8162
+ * LLM RTT.
8109
8163
  *
8110
8164
  * Fact + chunk lanes are fused with RRF (k=60). The previous naive
8111
8165
  * "sort the union by raw score" path is gone — score scales differ
@@ -8160,7 +8214,8 @@ interface RecallToolOptions {
8160
8214
  folderId?: string | null;
8161
8215
  /** Exclude one conversation from chunk results (typically the active one). */
8162
8216
  excludeConversationId?: string;
8163
- /** LLM-decompose options; only used at budget="high". Auth follows the
8217
+ /** LLM-decompose options; only used at budget="high". Runs in THIS tool
8218
+ * executor (719/B4) — `recall()` itself is LLM-free. Auth follows the
8164
8219
  * dual pattern: apiKey (server/CLI) or getToken (browser identity
8165
8220
  * tokens) — at least one required. */
8166
8221
  decomposeOptions?: PortalLlmAuth & {