@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.
@@ -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 & {
@@ -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,
@@ -1318,7 +1319,7 @@ __decorateClass([
1318
1319
  ], VaultFolder.prototype, "context", 2);
1319
1320
 
1320
1321
  // src/lib/db/schema.ts
1321
- var SDK_SCHEMA_VERSION = 42;
1322
+ var SDK_SCHEMA_VERSION = 43;
1322
1323
  var sdkSchema = (0, import_watermelondb12.appSchema)({
1323
1324
  version: SDK_SCHEMA_VERSION,
1324
1325
  tables: [
@@ -1366,7 +1367,16 @@ var sdkSchema = (0, import_watermelondb12.appSchema)({
1366
1367
  { name: "conversation_id", type: "string", isIndexed: true },
1367
1368
  { name: "title", type: "string" },
1368
1369
  { name: "project_id", type: "string", isOptional: true, isIndexed: true },
1369
- { name: "created_at", type: "number" },
1370
+ // Indexed to match every other list-sorted created_at in this schema.
1371
+ // Note this single-column index is NOT what makes the conversation list
1372
+ // reads fast on native SQLite — they all filter is_deleted as well, and
1373
+ // SQLite will not combine the two indexes, so the composite created by
1374
+ // the v42 migration is what its planner actually uses. This declaration
1375
+ // still earns its keep on the other two adapters: LokiJS builds its
1376
+ // binary indices from `isIndexed` (and ignores `sql` migration steps
1377
+ // outright), and Postgres gathers statistics via autovacuum, so its
1378
+ // planner can use a single-column index the SQLite planner skips.
1379
+ { name: "created_at", type: "number", isIndexed: true },
1370
1380
  { name: "updated_at", type: "number" },
1371
1381
  { name: "is_deleted", type: "boolean", isIndexed: true },
1372
1382
  { name: "pinned_at", type: "number", isOptional: true }
@@ -2233,6 +2243,49 @@ var sdkMigrations = (0, import_migrations.schemaMigrations)({
2233
2243
  ]
2234
2244
  })
2235
2245
  ]
2246
+ },
2247
+ // v42 -> v43: Give the conversation list reads an index they can actually
2248
+ // sort on. All five of them (getConversationsOp, getConversationsLazyOp,
2249
+ // getConversationsByProjectOp, getConversationsByProjectLazyOp and the
2250
+ // keyset getConversationsPageOp) emit `where is_deleted is 0 ... order by
2251
+ // created_at desc`, so before this they resolved is_deleted through its
2252
+ // index and then built a temp B-tree over every live row just to return
2253
+ // the newest page.
2254
+ //
2255
+ // The index is COMPOSITE, and that is the whole point — a bare index on
2256
+ // created_at does not fix this. SQLite will not combine two single-column
2257
+ // indexes here, so with `is_deleted` indexed and `created_at` indexed
2258
+ // separately it still picks conversations_is_deleted and still temp-sorts.
2259
+ // It only prefers a lone created_at index once ANALYZE has populated
2260
+ // sqlite_stat1, and nothing ever runs ANALYZE: not WatermelonDB, not its
2261
+ // native SQLite bindings, not this SDK. Every device is permanently in the
2262
+ // no-statistics state, so the index has to satisfy the filter and the sort
2263
+ // in one structure. `(is_deleted, created_at)` does: equality on the
2264
+ // leading column, ordered scan on the second, and the keyset boundary
2265
+ // becomes a range seek on the same index.
2266
+ //
2267
+ // unsafeExecuteSql, not addColumns: both columns already exist and
2268
+ // WatermelonDB has no add-index migration step, so raw SQL is the only way
2269
+ // to build one in place (same shape as the v19 -> v20 memory_vault
2270
+ // migration). IF NOT EXISTS keeps the step idempotent.
2271
+ //
2272
+ // Two limits worth knowing before someone re-measures this and finds it
2273
+ // missing. First, the LokiJS (web) adapter discards `sql` steps entirely,
2274
+ // so existing browser databases gain nothing — web's only lever is the
2275
+ // `isIndexed` flag on the column itself. Second, this index reaches
2276
+ // MIGRATED databases only: WatermelonDB builds a fresh database purely from
2277
+ // the encoded schema, and its schema format cannot express a composite
2278
+ // index. The one hook that could (`unsafeSql` on the table) is a function,
2279
+ // and the LokiJS adapter posts the schema to its worker, so attaching it
2280
+ // makes the whole schema fail structuredClone and takes the database down
2281
+ // at setup for anyone on `useWebWorker: true`. Not worth it for an index.
2282
+ {
2283
+ toVersion: 43,
2284
+ steps: [
2285
+ (0, import_migrations.unsafeExecuteSql)(
2286
+ "CREATE INDEX IF NOT EXISTS conversations_is_deleted_created_at ON conversations (is_deleted, created_at);"
2287
+ )
2288
+ ]
2236
2289
  }
2237
2290
  ]
2238
2291
  });
@@ -6586,7 +6639,7 @@ function createRecallTool(ctx, toolOptions, callbacks) {
6586
6639
  type: "function",
6587
6640
  function: {
6588
6641
  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.",
6642
+ 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
6643
  arguments: {
6591
6644
  type: "object",
6592
6645
  properties: {
@@ -6641,6 +6694,13 @@ function createRecallTool(ctx, toolOptions, callbacks) {
6641
6694
  let surfaced = 0;
6642
6695
  try {
6643
6696
  let recallDegraded = [];
6697
+ let subQueries;
6698
+ if (defaultBudget === "high" && toolOptions?.decomposeOptions) {
6699
+ const decomp = await decomposeQuery(query, toolOptions.decomposeOptions);
6700
+ if (decomp.mode === "composite" && decomp.subQueries.length >= 2) {
6701
+ subQueries = decomp.subQueries;
6702
+ }
6703
+ }
6644
6704
  const recallOpts = {
6645
6705
  onDiagnostics: (d) => {
6646
6706
  recallDegraded = d.degraded;
@@ -6654,10 +6714,12 @@ function createRecallTool(ctx, toolOptions, callbacks) {
6654
6714
  ...toolOptions?.excludeConversationId && {
6655
6715
  excludeConversationId: toolOptions.excludeConversationId
6656
6716
  },
6657
- ...toolOptions?.decomposeOptions && {
6658
- decomposeOptions: toolOptions.decomposeOptions
6659
- },
6660
- ...toolOptions?.now !== void 0 && { now: toolOptions.now }
6717
+ // Do NOT forward decomposeOptions into recall() — rewrite already
6718
+ // ran above, and forwarding would falsely trip `decompose-moved`
6719
+ // on every specific-mode high-budget call. graphRefine auth can
6720
+ // be threaded later when the tool opts into that path.
6721
+ ...toolOptions?.now !== void 0 && { now: toolOptions.now },
6722
+ ...subQueries && { subQueries }
6661
6723
  };
6662
6724
  const result = await recall(query, ctx, recallOpts);
6663
6725
  if (callbacks?.onChunksRetrieved) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  recall
3
- } from "./chunk-UQDYBQVF.mjs";
3
+ } from "./chunk-FKVYUOSD.mjs";
4
4
  import "./chunk-OBDRKQQI.mjs";
5
5
  import "./chunk-3RPGXXPZ.mjs";
6
6
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anuma/sdk",
3
- "version": "4.2.1",
3
+ "version": "5.0.1",
4
4
  "description": "",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",