@anuma/sdk 5.0.0 → 5.0.2

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.
@@ -55,12 +55,14 @@ async function consolidateMemory(newContent, candidates, options) {
55
55
  if (trimmed.length === 0) return fallback;
56
56
  if (candidates.length === 0) return fallback;
57
57
  const redactor = resolvePiiRedactor(options.piiRedaction);
58
- const safeTrimmed = redactor ? redactor.redactText(trimmed).text : trimmed;
59
- const candidateText = candidates.map((c, i) => {
60
- const safeContent = redactor ? redactor.redactText(c.content).text : c.content;
61
- return `[${i + 1}] (id: ${c.id}, sim: ${c.similarity.toFixed(2)})
62
- ${safeContent}`;
63
- }).join("\n");
58
+ const safeTrimmed = redactor ? (await redactor.redactTextAsync(trimmed)).text : trimmed;
59
+ const rows = [];
60
+ for (const [i, c] of candidates.entries()) {
61
+ const safeContent = redactor ? (await redactor.redactTextAsync(c.content)).text : c.content;
62
+ rows.push(`[${i + 1}] (id: ${c.id}, sim: ${c.similarity.toFixed(2)})
63
+ ${safeContent}`);
64
+ }
65
+ const candidateText = rows.join("\n");
64
66
  const userMessage = `New memory:
65
67
  ${safeTrimmed}
66
68
 
@@ -8755,12 +8755,14 @@ async function consolidateMemory(newContent, candidates, options) {
8755
8755
  if (trimmed.length === 0) return fallback;
8756
8756
  if (candidates.length === 0) return fallback;
8757
8757
  const redactor = resolvePiiRedactor(options.piiRedaction);
8758
- const safeTrimmed = redactor ? redactor.redactText(trimmed).text : trimmed;
8759
- const candidateText = candidates.map((c, i) => {
8760
- const safeContent = redactor ? redactor.redactText(c.content).text : c.content;
8761
- return `[${i + 1}] (id: ${c.id}, sim: ${c.similarity.toFixed(2)})
8762
- ${safeContent}`;
8763
- }).join("\n");
8758
+ const safeTrimmed = redactor ? (await redactor.redactTextAsync(trimmed)).text : trimmed;
8759
+ const rows = [];
8760
+ for (const [i, c] of candidates.entries()) {
8761
+ const safeContent = redactor ? (await redactor.redactTextAsync(c.content)).text : c.content;
8762
+ rows.push(`[${i + 1}] (id: ${c.id}, sim: ${c.similarity.toFixed(2)})
8763
+ ${safeContent}`);
8764
+ }
8765
+ const candidateText = rows.join("\n");
8764
8766
  const userMessage = `New memory:
8765
8767
  ${safeTrimmed}
8766
8768
 
@@ -15190,10 +15192,12 @@ async function classifyInjectionCandidates(candidates, options) {
15190
15192
  );
15191
15193
  }
15192
15194
  const redactor = resolvePiiRedactor(options.piiRedaction);
15193
- const numbered = scope.map((c, i) => {
15194
- const safe = redactor ? redactor.redactText(c.content).text : c.content;
15195
- return `[${i + 1}] ${safe}`;
15196
- }).join("\n");
15195
+ const lines = [];
15196
+ for (const [i, c] of scope.entries()) {
15197
+ const safe = redactor ? (await redactor.redactTextAsync(c.content)).text : c.content;
15198
+ lines.push(`[${i + 1}] ${safe}`);
15199
+ }
15200
+ const numbered = lines.join("\n");
15197
15201
  let parsed;
15198
15202
  try {
15199
15203
  parsed = await callPortalJsonCompletion({
@@ -16515,7 +16519,7 @@ function createLlmDecayClassifier(options) {
16515
16519
  }
16516
16520
  if (!content || content.trim().length === 0) return ruleVerdict;
16517
16521
  const trimmed = content.trim().slice(0, MAX_CONTENT_CHARS);
16518
- const safe = redactor ? redactor.redactText(trimmed).text : trimmed;
16522
+ const safe = redactor ? (await redactor.redactTextAsync(trimmed)).text : trimmed;
16519
16523
  const meta = `factType: ${input.factType ?? "none"}; ageDays: ${ageDays(input.updatedAt, now)}`;
16520
16524
  let parsed;
16521
16525
  try {
@@ -17645,17 +17649,22 @@ async function extractEntitiesForMemories(memories, options) {
17645
17649
  const out = /* @__PURE__ */ new Map();
17646
17650
  if (memories.length === 0) return out;
17647
17651
  const redactor = resolvePiiRedactor(options.piiRedaction);
17648
- const vocabulary = (options.existingEntityNames ?? []).filter((n) => n.trim().length > 0).slice(0, MAX_VOCABULARY_NAMES).map((n) => redactor ? redactor.redactText(n).text : n);
17652
+ const vocabulary = [];
17653
+ for (const name of (options.existingEntityNames ?? []).filter((n) => n.trim().length > 0).slice(0, MAX_VOCABULARY_NAMES)) {
17654
+ vocabulary.push(redactor ? (await redactor.redactTextAsync(name)).text : name);
17655
+ }
17649
17656
  const vocabularyNote = vocabulary.length > 0 ? `The user's existing topics: ${vocabulary.join(", ")}.
17650
17657
  When a memory refers to one of these, reuse the EXACT existing name (same spelling, casing, spacing and punctuation) instead of a variant \u2014 if "Foo.ai" is listed, never emit "Foo ai" or "Foo AI", and if "Jane Roe" is listed, emit that rather than a bare "Jane".
17651
17658
 
17652
17659
  ` : "";
17653
17660
  for (let i = 0; i < memories.length; i += TOPIC_EXTRACTION_BATCH_SIZE) {
17654
17661
  const batch = memories.slice(i, i + TOPIC_EXTRACTION_BATCH_SIZE);
17655
- const listing = batch.map((m) => {
17662
+ const rows = [];
17663
+ for (const m of batch) {
17656
17664
  const content = m.content.slice(0, MAX_CHARS_PER_MEMORY).replace(/\s+/g, " ").trim();
17657
- return `${m.id}: ${redactor ? redactor.redactText(content).text : content}`;
17658
- }).join("\n");
17665
+ rows.push(`${m.id}: ${redactor ? (await redactor.redactTextAsync(content)).text : content}`);
17666
+ }
17667
+ const listing = rows.join("\n");
17659
17668
  const parsed = await callPortalJsonCompletion({
17660
17669
  ...options.apiKey !== void 0 && { apiKey: options.apiKey },
17661
17670
  ...options.getToken !== void 0 && { getToken: options.getToken },
@@ -21056,7 +21065,7 @@ __decorateClass([
21056
21065
  ], VaultFolder.prototype, "context", 2);
21057
21066
 
21058
21067
  // src/lib/db/schema.ts
21059
- var SDK_SCHEMA_VERSION = 42;
21068
+ var SDK_SCHEMA_VERSION = 43;
21060
21069
  var sdkSchema = (0, import_watermelondb19.appSchema)({
21061
21070
  version: SDK_SCHEMA_VERSION,
21062
21071
  tables: [
@@ -21104,7 +21113,16 @@ var sdkSchema = (0, import_watermelondb19.appSchema)({
21104
21113
  { name: "conversation_id", type: "string", isIndexed: true },
21105
21114
  { name: "title", type: "string" },
21106
21115
  { name: "project_id", type: "string", isOptional: true, isIndexed: true },
21107
- { name: "created_at", type: "number" },
21116
+ // Indexed to match every other list-sorted created_at in this schema.
21117
+ // Note this single-column index is NOT what makes the conversation list
21118
+ // reads fast on native SQLite — they all filter is_deleted as well, and
21119
+ // SQLite will not combine the two indexes, so the composite created by
21120
+ // the v42 migration is what its planner actually uses. This declaration
21121
+ // still earns its keep on the other two adapters: LokiJS builds its
21122
+ // binary indices from `isIndexed` (and ignores `sql` migration steps
21123
+ // outright), and Postgres gathers statistics via autovacuum, so its
21124
+ // planner can use a single-column index the SQLite planner skips.
21125
+ { name: "created_at", type: "number", isIndexed: true },
21108
21126
  { name: "updated_at", type: "number" },
21109
21127
  { name: "is_deleted", type: "boolean", isIndexed: true },
21110
21128
  { name: "pinned_at", type: "number", isOptional: true }
@@ -21971,6 +21989,49 @@ var sdkMigrations = (0, import_migrations2.schemaMigrations)({
21971
21989
  ]
21972
21990
  })
21973
21991
  ]
21992
+ },
21993
+ // v42 -> v43: Give the conversation list reads an index they can actually
21994
+ // sort on. All five of them (getConversationsOp, getConversationsLazyOp,
21995
+ // getConversationsByProjectOp, getConversationsByProjectLazyOp and the
21996
+ // keyset getConversationsPageOp) emit `where is_deleted is 0 ... order by
21997
+ // created_at desc`, so before this they resolved is_deleted through its
21998
+ // index and then built a temp B-tree over every live row just to return
21999
+ // the newest page.
22000
+ //
22001
+ // The index is COMPOSITE, and that is the whole point — a bare index on
22002
+ // created_at does not fix this. SQLite will not combine two single-column
22003
+ // indexes here, so with `is_deleted` indexed and `created_at` indexed
22004
+ // separately it still picks conversations_is_deleted and still temp-sorts.
22005
+ // It only prefers a lone created_at index once ANALYZE has populated
22006
+ // sqlite_stat1, and nothing ever runs ANALYZE: not WatermelonDB, not its
22007
+ // native SQLite bindings, not this SDK. Every device is permanently in the
22008
+ // no-statistics state, so the index has to satisfy the filter and the sort
22009
+ // in one structure. `(is_deleted, created_at)` does: equality on the
22010
+ // leading column, ordered scan on the second, and the keyset boundary
22011
+ // becomes a range seek on the same index.
22012
+ //
22013
+ // unsafeExecuteSql, not addColumns: both columns already exist and
22014
+ // WatermelonDB has no add-index migration step, so raw SQL is the only way
22015
+ // to build one in place (same shape as the v19 -> v20 memory_vault
22016
+ // migration). IF NOT EXISTS keeps the step idempotent.
22017
+ //
22018
+ // Two limits worth knowing before someone re-measures this and finds it
22019
+ // missing. First, the LokiJS (web) adapter discards `sql` steps entirely,
22020
+ // so existing browser databases gain nothing — web's only lever is the
22021
+ // `isIndexed` flag on the column itself. Second, this index reaches
22022
+ // MIGRATED databases only: WatermelonDB builds a fresh database purely from
22023
+ // the encoded schema, and its schema format cannot express a composite
22024
+ // index. The one hook that could (`unsafeSql` on the table) is a function,
22025
+ // and the LokiJS adapter posts the schema to its worker, so attaching it
22026
+ // makes the whole schema fail structuredClone and takes the database down
22027
+ // at setup for anyone on `useWebWorker: true`. Not worth it for an index.
22028
+ {
22029
+ toVersion: 43,
22030
+ steps: [
22031
+ (0, import_migrations2.unsafeExecuteSql)(
22032
+ "CREATE INDEX IF NOT EXISTS conversations_is_deleted_created_at ON conversations (is_deleted, created_at);"
22033
+ )
22034
+ ]
21974
22035
  }
21975
22036
  ]
21976
22037
  });
@@ -8779,8 +8779,12 @@ declare function useChatStorage(options: UseChatStorageOptions): UseChatStorageR
8779
8779
  * `topics_updated_at` is a SECOND timestamp because every topic writer pins
8780
8780
  * `updated_at` on purpose (recall's recency multiplier) and both sync paths
8781
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
8782
8786
  */
8783
- declare const SDK_SCHEMA_VERSION = 42;
8787
+ declare const SDK_SCHEMA_VERSION = 43;
8784
8788
  /**
8785
8789
  * Combined WatermelonDB schema for all SDK storage modules.
8786
8790
  *
@@ -8867,6 +8871,7 @@ declare const sdkSchema: Readonly<{
8867
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)
8868
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')
8869
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)
8870
8875
  */
8871
8876
  declare const sdkMigrations: Readonly<{
8872
8877
  validated: true;
@@ -8779,8 +8779,12 @@ declare function useChatStorage(options: UseChatStorageOptions): UseChatStorageR
8779
8779
  * `topics_updated_at` is a SECOND timestamp because every topic writer pins
8780
8780
  * `updated_at` on purpose (recall's recency multiplier) and both sync paths
8781
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
8782
8786
  */
8783
- declare const SDK_SCHEMA_VERSION = 42;
8787
+ declare const SDK_SCHEMA_VERSION = 43;
8784
8788
  /**
8785
8789
  * Combined WatermelonDB schema for all SDK storage modules.
8786
8790
  *
@@ -8867,6 +8871,7 @@ declare const sdkSchema: Readonly<{
8867
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)
8868
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')
8869
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)
8870
8875
  */
8871
8876
  declare const sdkMigrations: Readonly<{
8872
8877
  validated: true;
@@ -6193,10 +6193,12 @@ async function classifyInjectionCandidates(candidates, options) {
6193
6193
  );
6194
6194
  }
6195
6195
  const redactor = resolvePiiRedactor(options.piiRedaction);
6196
- const numbered = scope.map((c, i) => {
6197
- const safe = redactor ? redactor.redactText(c.content).text : c.content;
6198
- return `[${i + 1}] ${safe}`;
6199
- }).join("\n");
6196
+ const lines = [];
6197
+ for (const [i, c] of scope.entries()) {
6198
+ const safe = redactor ? (await redactor.redactTextAsync(c.content)).text : c.content;
6199
+ lines.push(`[${i + 1}] ${safe}`);
6200
+ }
6201
+ const numbered = lines.join("\n");
6200
6202
  let parsed;
6201
6203
  try {
6202
6204
  parsed = await callPortalJsonCompletion({
@@ -6677,7 +6679,7 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
6677
6679
  content: m.content,
6678
6680
  similarity: m.similarity
6679
6681
  }));
6680
- const { consolidateMemory: doConsolidate } = await import("./consolidate-KASO7JDS.mjs");
6682
+ const { consolidateMemory: doConsolidate } = await import("./consolidate-T6GZ5YQ4.mjs");
6681
6683
  const decision = await doConsolidate(trimmed, candidates, consolidateOptions);
6682
6684
  if (decision.action === "create") return null;
6683
6685
  if (decision.action === "supersede" && decision.content) {
@@ -7501,7 +7503,7 @@ function createLlmDecayClassifier(options) {
7501
7503
  }
7502
7504
  if (!content || content.trim().length === 0) return ruleVerdict;
7503
7505
  const trimmed = content.trim().slice(0, MAX_CONTENT_CHARS);
7504
- const safe = redactor ? redactor.redactText(trimmed).text : trimmed;
7506
+ const safe = redactor ? (await redactor.redactTextAsync(trimmed)).text : trimmed;
7505
7507
  const meta = `factType: ${input.factType ?? "none"}; ageDays: ${ageDays(input.updatedAt, now)}`;
7506
7508
  let parsed;
7507
7509
  try {
@@ -8598,17 +8600,22 @@ async function extractEntitiesForMemories(memories, options) {
8598
8600
  const out = /* @__PURE__ */ new Map();
8599
8601
  if (memories.length === 0) return out;
8600
8602
  const redactor = resolvePiiRedactor(options.piiRedaction);
8601
- const vocabulary = (options.existingEntityNames ?? []).filter((n) => n.trim().length > 0).slice(0, MAX_VOCABULARY_NAMES).map((n) => redactor ? redactor.redactText(n).text : n);
8603
+ const vocabulary = [];
8604
+ for (const name of (options.existingEntityNames ?? []).filter((n) => n.trim().length > 0).slice(0, MAX_VOCABULARY_NAMES)) {
8605
+ vocabulary.push(redactor ? (await redactor.redactTextAsync(name)).text : name);
8606
+ }
8602
8607
  const vocabularyNote = vocabulary.length > 0 ? `The user's existing topics: ${vocabulary.join(", ")}.
8603
8608
  When a memory refers to one of these, reuse the EXACT existing name (same spelling, casing, spacing and punctuation) instead of a variant \u2014 if "Foo.ai" is listed, never emit "Foo ai" or "Foo AI", and if "Jane Roe" is listed, emit that rather than a bare "Jane".
8604
8609
 
8605
8610
  ` : "";
8606
8611
  for (let i = 0; i < memories.length; i += TOPIC_EXTRACTION_BATCH_SIZE) {
8607
8612
  const batch = memories.slice(i, i + TOPIC_EXTRACTION_BATCH_SIZE);
8608
- const listing = batch.map((m) => {
8613
+ const rows = [];
8614
+ for (const m of batch) {
8609
8615
  const content = m.content.slice(0, MAX_CHARS_PER_MEMORY).replace(/\s+/g, " ").trim();
8610
- return `${m.id}: ${redactor ? redactor.redactText(content).text : content}`;
8611
- }).join("\n");
8616
+ rows.push(`${m.id}: ${redactor ? (await redactor.redactTextAsync(content)).text : content}`);
8617
+ }
8618
+ const listing = rows.join("\n");
8612
8619
  const parsed = await callPortalJsonCompletion({
8613
8620
  ...options.apiKey !== void 0 && { apiKey: options.apiKey },
8614
8621
  ...options.getToken !== void 0 && { getToken: options.getToken },
@@ -11980,7 +11987,7 @@ __decorateClass([
11980
11987
  ], VaultFolder.prototype, "context", 2);
11981
11988
 
11982
11989
  // src/lib/db/schema.ts
11983
- var SDK_SCHEMA_VERSION = 42;
11990
+ var SDK_SCHEMA_VERSION = 43;
11984
11991
  var sdkSchema = appSchema2({
11985
11992
  version: SDK_SCHEMA_VERSION,
11986
11993
  tables: [
@@ -12028,7 +12035,16 @@ var sdkSchema = appSchema2({
12028
12035
  { name: "conversation_id", type: "string", isIndexed: true },
12029
12036
  { name: "title", type: "string" },
12030
12037
  { name: "project_id", type: "string", isOptional: true, isIndexed: true },
12031
- { name: "created_at", type: "number" },
12038
+ // Indexed to match every other list-sorted created_at in this schema.
12039
+ // Note this single-column index is NOT what makes the conversation list
12040
+ // reads fast on native SQLite — they all filter is_deleted as well, and
12041
+ // SQLite will not combine the two indexes, so the composite created by
12042
+ // the v42 migration is what its planner actually uses. This declaration
12043
+ // still earns its keep on the other two adapters: LokiJS builds its
12044
+ // binary indices from `isIndexed` (and ignores `sql` migration steps
12045
+ // outright), and Postgres gathers statistics via autovacuum, so its
12046
+ // planner can use a single-column index the SQLite planner skips.
12047
+ { name: "created_at", type: "number", isIndexed: true },
12032
12048
  { name: "updated_at", type: "number" },
12033
12049
  { name: "is_deleted", type: "boolean", isIndexed: true },
12034
12050
  { name: "pinned_at", type: "number", isOptional: true }
@@ -12895,6 +12911,49 @@ var sdkMigrations = schemaMigrations2({
12895
12911
  ]
12896
12912
  })
12897
12913
  ]
12914
+ },
12915
+ // v42 -> v43: Give the conversation list reads an index they can actually
12916
+ // sort on. All five of them (getConversationsOp, getConversationsLazyOp,
12917
+ // getConversationsByProjectOp, getConversationsByProjectLazyOp and the
12918
+ // keyset getConversationsPageOp) emit `where is_deleted is 0 ... order by
12919
+ // created_at desc`, so before this they resolved is_deleted through its
12920
+ // index and then built a temp B-tree over every live row just to return
12921
+ // the newest page.
12922
+ //
12923
+ // The index is COMPOSITE, and that is the whole point — a bare index on
12924
+ // created_at does not fix this. SQLite will not combine two single-column
12925
+ // indexes here, so with `is_deleted` indexed and `created_at` indexed
12926
+ // separately it still picks conversations_is_deleted and still temp-sorts.
12927
+ // It only prefers a lone created_at index once ANALYZE has populated
12928
+ // sqlite_stat1, and nothing ever runs ANALYZE: not WatermelonDB, not its
12929
+ // native SQLite bindings, not this SDK. Every device is permanently in the
12930
+ // no-statistics state, so the index has to satisfy the filter and the sort
12931
+ // in one structure. `(is_deleted, created_at)` does: equality on the
12932
+ // leading column, ordered scan on the second, and the keyset boundary
12933
+ // becomes a range seek on the same index.
12934
+ //
12935
+ // unsafeExecuteSql, not addColumns: both columns already exist and
12936
+ // WatermelonDB has no add-index migration step, so raw SQL is the only way
12937
+ // to build one in place (same shape as the v19 -> v20 memory_vault
12938
+ // migration). IF NOT EXISTS keeps the step idempotent.
12939
+ //
12940
+ // Two limits worth knowing before someone re-measures this and finds it
12941
+ // missing. First, the LokiJS (web) adapter discards `sql` steps entirely,
12942
+ // so existing browser databases gain nothing — web's only lever is the
12943
+ // `isIndexed` flag on the column itself. Second, this index reaches
12944
+ // MIGRATED databases only: WatermelonDB builds a fresh database purely from
12945
+ // the encoded schema, and its schema format cannot express a composite
12946
+ // index. The one hook that could (`unsafeSql` on the table) is a function,
12947
+ // and the LokiJS adapter posts the schema to its worker, so attaching it
12948
+ // makes the whole schema fail structuredClone and takes the database down
12949
+ // at setup for anyone on `useWebWorker: true`. Not worth it for an index.
12950
+ {
12951
+ toVersion: 43,
12952
+ steps: [
12953
+ unsafeExecuteSql(
12954
+ "CREATE INDEX IF NOT EXISTS conversations_is_deleted_created_at ON conversations (is_deleted, created_at);"
12955
+ )
12956
+ ]
12898
12957
  }
12899
12958
  ]
12900
12959
  });
@@ -55,12 +55,14 @@ async function consolidateMemory(newContent, candidates, options) {
55
55
  if (trimmed.length === 0) return fallback;
56
56
  if (candidates.length === 0) return fallback;
57
57
  const redactor = resolvePiiRedactor(options.piiRedaction);
58
- const safeTrimmed = redactor ? redactor.redactText(trimmed).text : trimmed;
59
- const candidateText = candidates.map((c, i) => {
60
- const safeContent = redactor ? redactor.redactText(c.content).text : c.content;
61
- return `[${i + 1}] (id: ${c.id}, sim: ${c.similarity.toFixed(2)})
62
- ${safeContent}`;
63
- }).join("\n");
58
+ const safeTrimmed = redactor ? (await redactor.redactTextAsync(trimmed)).text : trimmed;
59
+ const rows = [];
60
+ for (const [i, c] of candidates.entries()) {
61
+ const safeContent = redactor ? (await redactor.redactTextAsync(c.content)).text : c.content;
62
+ rows.push(`[${i + 1}] (id: ${c.id}, sim: ${c.similarity.toFixed(2)})
63
+ ${safeContent}`);
64
+ }
65
+ const candidateText = rows.join("\n");
64
66
  const userMessage = `New memory:
65
67
  ${safeTrimmed}
66
68
 
@@ -9038,12 +9038,14 @@ async function consolidateMemory(newContent, candidates, options) {
9038
9038
  if (trimmed.length === 0) return fallback;
9039
9039
  if (candidates.length === 0) return fallback;
9040
9040
  const redactor = resolvePiiRedactor(options.piiRedaction);
9041
- const safeTrimmed = redactor ? redactor.redactText(trimmed).text : trimmed;
9042
- const candidateText = candidates.map((c, i) => {
9043
- const safeContent = redactor ? redactor.redactText(c.content).text : c.content;
9044
- return `[${i + 1}] (id: ${c.id}, sim: ${c.similarity.toFixed(2)})
9045
- ${safeContent}`;
9046
- }).join("\n");
9041
+ const safeTrimmed = redactor ? (await redactor.redactTextAsync(trimmed)).text : trimmed;
9042
+ const rows = [];
9043
+ for (const [i, c] of candidates.entries()) {
9044
+ const safeContent = redactor ? (await redactor.redactTextAsync(c.content)).text : c.content;
9045
+ rows.push(`[${i + 1}] (id: ${c.id}, sim: ${c.similarity.toFixed(2)})
9046
+ ${safeContent}`);
9047
+ }
9048
+ const candidateText = rows.join("\n");
9047
9049
  const userMessage = `New memory:
9048
9050
  ${safeTrimmed}
9049
9051
 
@@ -16489,10 +16491,12 @@ async function classifyInjectionCandidates(candidates, options) {
16489
16491
  );
16490
16492
  }
16491
16493
  const redactor = resolvePiiRedactor(options.piiRedaction);
16492
- const numbered = scope.map((c, i) => {
16493
- const safe = redactor ? redactor.redactText(c.content).text : c.content;
16494
- return `[${i + 1}] ${safe}`;
16495
- }).join("\n");
16494
+ const lines = [];
16495
+ for (const [i, c] of scope.entries()) {
16496
+ const safe = redactor ? (await redactor.redactTextAsync(c.content)).text : c.content;
16497
+ lines.push(`[${i + 1}] ${safe}`);
16498
+ }
16499
+ const numbered = lines.join("\n");
16496
16500
  let parsed;
16497
16501
  try {
16498
16502
  parsed = await callPortalJsonCompletion({
@@ -17814,7 +17818,7 @@ function createLlmDecayClassifier(options) {
17814
17818
  }
17815
17819
  if (!content || content.trim().length === 0) return ruleVerdict;
17816
17820
  const trimmed = content.trim().slice(0, MAX_CONTENT_CHARS);
17817
- const safe = redactor ? redactor.redactText(trimmed).text : trimmed;
17821
+ const safe = redactor ? (await redactor.redactTextAsync(trimmed)).text : trimmed;
17818
17822
  const meta = `factType: ${input.factType ?? "none"}; ageDays: ${ageDays(input.updatedAt, now)}`;
17819
17823
  let parsed;
17820
17824
  try {
@@ -18944,17 +18948,22 @@ async function extractEntitiesForMemories(memories, options) {
18944
18948
  const out = /* @__PURE__ */ new Map();
18945
18949
  if (memories.length === 0) return out;
18946
18950
  const redactor = resolvePiiRedactor(options.piiRedaction);
18947
- const vocabulary = (options.existingEntityNames ?? []).filter((n) => n.trim().length > 0).slice(0, MAX_VOCABULARY_NAMES).map((n) => redactor ? redactor.redactText(n).text : n);
18951
+ const vocabulary = [];
18952
+ for (const name of (options.existingEntityNames ?? []).filter((n) => n.trim().length > 0).slice(0, MAX_VOCABULARY_NAMES)) {
18953
+ vocabulary.push(redactor ? (await redactor.redactTextAsync(name)).text : name);
18954
+ }
18948
18955
  const vocabularyNote = vocabulary.length > 0 ? `The user's existing topics: ${vocabulary.join(", ")}.
18949
18956
  When a memory refers to one of these, reuse the EXACT existing name (same spelling, casing, spacing and punctuation) instead of a variant \u2014 if "Foo.ai" is listed, never emit "Foo ai" or "Foo AI", and if "Jane Roe" is listed, emit that rather than a bare "Jane".
18950
18957
 
18951
18958
  ` : "";
18952
18959
  for (let i = 0; i < memories.length; i += TOPIC_EXTRACTION_BATCH_SIZE) {
18953
18960
  const batch = memories.slice(i, i + TOPIC_EXTRACTION_BATCH_SIZE);
18954
- const listing = batch.map((m) => {
18961
+ const rows = [];
18962
+ for (const m of batch) {
18955
18963
  const content = m.content.slice(0, MAX_CHARS_PER_MEMORY).replace(/\s+/g, " ").trim();
18956
- return `${m.id}: ${redactor ? redactor.redactText(content).text : content}`;
18957
- }).join("\n");
18964
+ rows.push(`${m.id}: ${redactor ? (await redactor.redactTextAsync(content)).text : content}`);
18965
+ }
18966
+ const listing = rows.join("\n");
18958
18967
  const parsed = await callPortalJsonCompletion({
18959
18968
  ...options.apiKey !== void 0 && { apiKey: options.apiKey },
18960
18969
  ...options.getToken !== void 0 && { getToken: options.getToken },
@@ -23629,7 +23638,7 @@ __decorateClass([
23629
23638
  ], VaultFolder.prototype, "context", 2);
23630
23639
 
23631
23640
  // src/lib/db/schema.ts
23632
- var SDK_SCHEMA_VERSION = 42;
23641
+ var SDK_SCHEMA_VERSION = 43;
23633
23642
  var sdkSchema = (0, import_watermelondb20.appSchema)({
23634
23643
  version: SDK_SCHEMA_VERSION,
23635
23644
  tables: [
@@ -23677,7 +23686,16 @@ var sdkSchema = (0, import_watermelondb20.appSchema)({
23677
23686
  { name: "conversation_id", type: "string", isIndexed: true },
23678
23687
  { name: "title", type: "string" },
23679
23688
  { name: "project_id", type: "string", isOptional: true, isIndexed: true },
23680
- { name: "created_at", type: "number" },
23689
+ // Indexed to match every other list-sorted created_at in this schema.
23690
+ // Note this single-column index is NOT what makes the conversation list
23691
+ // reads fast on native SQLite — they all filter is_deleted as well, and
23692
+ // SQLite will not combine the two indexes, so the composite created by
23693
+ // the v42 migration is what its planner actually uses. This declaration
23694
+ // still earns its keep on the other two adapters: LokiJS builds its
23695
+ // binary indices from `isIndexed` (and ignores `sql` migration steps
23696
+ // outright), and Postgres gathers statistics via autovacuum, so its
23697
+ // planner can use a single-column index the SQLite planner skips.
23698
+ { name: "created_at", type: "number", isIndexed: true },
23681
23699
  { name: "updated_at", type: "number" },
23682
23700
  { name: "is_deleted", type: "boolean", isIndexed: true },
23683
23701
  { name: "pinned_at", type: "number", isOptional: true }
@@ -24544,6 +24562,49 @@ var sdkMigrations = (0, import_migrations2.schemaMigrations)({
24544
24562
  ]
24545
24563
  })
24546
24564
  ]
24565
+ },
24566
+ // v42 -> v43: Give the conversation list reads an index they can actually
24567
+ // sort on. All five of them (getConversationsOp, getConversationsLazyOp,
24568
+ // getConversationsByProjectOp, getConversationsByProjectLazyOp and the
24569
+ // keyset getConversationsPageOp) emit `where is_deleted is 0 ... order by
24570
+ // created_at desc`, so before this they resolved is_deleted through its
24571
+ // index and then built a temp B-tree over every live row just to return
24572
+ // the newest page.
24573
+ //
24574
+ // The index is COMPOSITE, and that is the whole point — a bare index on
24575
+ // created_at does not fix this. SQLite will not combine two single-column
24576
+ // indexes here, so with `is_deleted` indexed and `created_at` indexed
24577
+ // separately it still picks conversations_is_deleted and still temp-sorts.
24578
+ // It only prefers a lone created_at index once ANALYZE has populated
24579
+ // sqlite_stat1, and nothing ever runs ANALYZE: not WatermelonDB, not its
24580
+ // native SQLite bindings, not this SDK. Every device is permanently in the
24581
+ // no-statistics state, so the index has to satisfy the filter and the sort
24582
+ // in one structure. `(is_deleted, created_at)` does: equality on the
24583
+ // leading column, ordered scan on the second, and the keyset boundary
24584
+ // becomes a range seek on the same index.
24585
+ //
24586
+ // unsafeExecuteSql, not addColumns: both columns already exist and
24587
+ // WatermelonDB has no add-index migration step, so raw SQL is the only way
24588
+ // to build one in place (same shape as the v19 -> v20 memory_vault
24589
+ // migration). IF NOT EXISTS keeps the step idempotent.
24590
+ //
24591
+ // Two limits worth knowing before someone re-measures this and finds it
24592
+ // missing. First, the LokiJS (web) adapter discards `sql` steps entirely,
24593
+ // so existing browser databases gain nothing — web's only lever is the
24594
+ // `isIndexed` flag on the column itself. Second, this index reaches
24595
+ // MIGRATED databases only: WatermelonDB builds a fresh database purely from
24596
+ // the encoded schema, and its schema format cannot express a composite
24597
+ // index. The one hook that could (`unsafeSql` on the table) is a function,
24598
+ // and the LokiJS adapter posts the schema to its worker, so attaching it
24599
+ // makes the whole schema fail structuredClone and takes the database down
24600
+ // at setup for anyone on `useWebWorker: true`. Not worth it for an index.
24601
+ {
24602
+ toVersion: 43,
24603
+ steps: [
24604
+ (0, import_migrations2.unsafeExecuteSql)(
24605
+ "CREATE INDEX IF NOT EXISTS conversations_is_deleted_created_at ON conversations (is_deleted, created_at);"
24606
+ )
24607
+ ]
24547
24608
  }
24548
24609
  ]
24549
24610
  });
@@ -10393,8 +10393,12 @@ declare class WatermelonChatStorageAdapter implements ChatStorageAdapter {
10393
10393
  * `topics_updated_at` is a SECOND timestamp because every topic writer pins
10394
10394
  * `updated_at` on purpose (recall's recency multiplier) and both sync paths
10395
10395
  * key on `updated_at`, so a topic-only change would neither upload nor merge
10396
+ * - v43: Added a (is_deleted, created_at) index to conversations. Every
10397
+ * conversation list read filters is_deleted and orders by created_at DESC,
10398
+ * which previously meant a temp B-tree sort of the whole live set on every
10399
+ * read. Structural only — no column added, no data rewritten
10396
10400
  */
10397
- declare const SDK_SCHEMA_VERSION = 42;
10401
+ declare const SDK_SCHEMA_VERSION = 43;
10398
10402
  /**
10399
10403
  * Combined WatermelonDB schema for all SDK storage modules.
10400
10404
  *
@@ -10481,6 +10485,7 @@ declare const sdkSchema: Readonly<{
10481
10485
  * - 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)
10482
10486
  * - 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')
10483
10487
  * - 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)
10488
+ * - v42 → v43: Added a composite `(is_deleted, created_at)` index to conversations so the list reads stop temp-sorting (structural only, no data rewritten)
10484
10489
  */
10485
10490
  declare const sdkMigrations: Readonly<{
10486
10491
  validated: true;
@@ -10393,8 +10393,12 @@ declare class WatermelonChatStorageAdapter implements ChatStorageAdapter {
10393
10393
  * `topics_updated_at` is a SECOND timestamp because every topic writer pins
10394
10394
  * `updated_at` on purpose (recall's recency multiplier) and both sync paths
10395
10395
  * key on `updated_at`, so a topic-only change would neither upload nor merge
10396
+ * - v43: Added a (is_deleted, created_at) index to conversations. Every
10397
+ * conversation list read filters is_deleted and orders by created_at DESC,
10398
+ * which previously meant a temp B-tree sort of the whole live set on every
10399
+ * read. Structural only — no column added, no data rewritten
10396
10400
  */
10397
- declare const SDK_SCHEMA_VERSION = 42;
10401
+ declare const SDK_SCHEMA_VERSION = 43;
10398
10402
  /**
10399
10403
  * Combined WatermelonDB schema for all SDK storage modules.
10400
10404
  *
@@ -10481,6 +10485,7 @@ declare const sdkSchema: Readonly<{
10481
10485
  * - 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)
10482
10486
  * - 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')
10483
10487
  * - 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)
10488
+ * - v42 → v43: Added a composite `(is_deleted, created_at)` index to conversations so the list reads stop temp-sorting (structural only, no data rewritten)
10484
10489
  */
10485
10490
  declare const sdkMigrations: Readonly<{
10486
10491
  validated: true;
@@ -7010,10 +7010,12 @@ async function classifyInjectionCandidates(candidates, options) {
7010
7010
  );
7011
7011
  }
7012
7012
  const redactor = resolvePiiRedactor(options.piiRedaction);
7013
- const numbered = scope.map((c, i) => {
7014
- const safe = redactor ? redactor.redactText(c.content).text : c.content;
7015
- return `[${i + 1}] ${safe}`;
7016
- }).join("\n");
7013
+ const lines = [];
7014
+ for (const [i, c] of scope.entries()) {
7015
+ const safe = redactor ? (await redactor.redactTextAsync(c.content)).text : c.content;
7016
+ lines.push(`[${i + 1}] ${safe}`);
7017
+ }
7018
+ const numbered = lines.join("\n");
7017
7019
  let parsed;
7018
7020
  try {
7019
7021
  parsed = await callPortalJsonCompletion({
@@ -7494,7 +7496,7 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
7494
7496
  content: m.content,
7495
7497
  similarity: m.similarity
7496
7498
  }));
7497
- const { consolidateMemory: doConsolidate } = await import("./consolidate-IFIKT25Z.mjs");
7499
+ const { consolidateMemory: doConsolidate } = await import("./consolidate-POPTUI54.mjs");
7498
7500
  const decision = await doConsolidate(trimmed, candidates, consolidateOptions);
7499
7501
  if (decision.action === "create") return null;
7500
7502
  if (decision.action === "supersede" && decision.content) {
@@ -8318,7 +8320,7 @@ function createLlmDecayClassifier(options) {
8318
8320
  }
8319
8321
  if (!content || content.trim().length === 0) return ruleVerdict;
8320
8322
  const trimmed = content.trim().slice(0, MAX_CONTENT_CHARS);
8321
- const safe = redactor ? redactor.redactText(trimmed).text : trimmed;
8323
+ const safe = redactor ? (await redactor.redactTextAsync(trimmed)).text : trimmed;
8322
8324
  const meta = `factType: ${input.factType ?? "none"}; ageDays: ${ageDays(input.updatedAt, now)}`;
8323
8325
  let parsed;
8324
8326
  try {
@@ -9415,17 +9417,22 @@ async function extractEntitiesForMemories(memories, options) {
9415
9417
  const out = /* @__PURE__ */ new Map();
9416
9418
  if (memories.length === 0) return out;
9417
9419
  const redactor = resolvePiiRedactor(options.piiRedaction);
9418
- const vocabulary = (options.existingEntityNames ?? []).filter((n) => n.trim().length > 0).slice(0, MAX_VOCABULARY_NAMES).map((n) => redactor ? redactor.redactText(n).text : n);
9420
+ const vocabulary = [];
9421
+ for (const name of (options.existingEntityNames ?? []).filter((n) => n.trim().length > 0).slice(0, MAX_VOCABULARY_NAMES)) {
9422
+ vocabulary.push(redactor ? (await redactor.redactTextAsync(name)).text : name);
9423
+ }
9419
9424
  const vocabularyNote = vocabulary.length > 0 ? `The user's existing topics: ${vocabulary.join(", ")}.
9420
9425
  When a memory refers to one of these, reuse the EXACT existing name (same spelling, casing, spacing and punctuation) instead of a variant \u2014 if "Foo.ai" is listed, never emit "Foo ai" or "Foo AI", and if "Jane Roe" is listed, emit that rather than a bare "Jane".
9421
9426
 
9422
9427
  ` : "";
9423
9428
  for (let i = 0; i < memories.length; i += TOPIC_EXTRACTION_BATCH_SIZE) {
9424
9429
  const batch = memories.slice(i, i + TOPIC_EXTRACTION_BATCH_SIZE);
9425
- const listing = batch.map((m) => {
9430
+ const rows = [];
9431
+ for (const m of batch) {
9426
9432
  const content = m.content.slice(0, MAX_CHARS_PER_MEMORY).replace(/\s+/g, " ").trim();
9427
- return `${m.id}: ${redactor ? redactor.redactText(content).text : content}`;
9428
- }).join("\n");
9433
+ rows.push(`${m.id}: ${redactor ? (await redactor.redactTextAsync(content)).text : content}`);
9434
+ }
9435
+ const listing = rows.join("\n");
9429
9436
  const parsed = await callPortalJsonCompletion({
9430
9437
  ...options.apiKey !== void 0 && { apiKey: options.apiKey },
9431
9438
  ...options.getToken !== void 0 && { getToken: options.getToken },
@@ -14064,7 +14071,7 @@ __decorateClass([
14064
14071
  ], VaultFolder.prototype, "context", 2);
14065
14072
 
14066
14073
  // src/lib/db/schema.ts
14067
- var SDK_SCHEMA_VERSION = 42;
14074
+ var SDK_SCHEMA_VERSION = 43;
14068
14075
  var sdkSchema = appSchema2({
14069
14076
  version: SDK_SCHEMA_VERSION,
14070
14077
  tables: [
@@ -14112,7 +14119,16 @@ var sdkSchema = appSchema2({
14112
14119
  { name: "conversation_id", type: "string", isIndexed: true },
14113
14120
  { name: "title", type: "string" },
14114
14121
  { name: "project_id", type: "string", isOptional: true, isIndexed: true },
14115
- { name: "created_at", type: "number" },
14122
+ // Indexed to match every other list-sorted created_at in this schema.
14123
+ // Note this single-column index is NOT what makes the conversation list
14124
+ // reads fast on native SQLite — they all filter is_deleted as well, and
14125
+ // SQLite will not combine the two indexes, so the composite created by
14126
+ // the v42 migration is what its planner actually uses. This declaration
14127
+ // still earns its keep on the other two adapters: LokiJS builds its
14128
+ // binary indices from `isIndexed` (and ignores `sql` migration steps
14129
+ // outright), and Postgres gathers statistics via autovacuum, so its
14130
+ // planner can use a single-column index the SQLite planner skips.
14131
+ { name: "created_at", type: "number", isIndexed: true },
14116
14132
  { name: "updated_at", type: "number" },
14117
14133
  { name: "is_deleted", type: "boolean", isIndexed: true },
14118
14134
  { name: "pinned_at", type: "number", isOptional: true }
@@ -14979,6 +14995,49 @@ var sdkMigrations = schemaMigrations2({
14979
14995
  ]
14980
14996
  })
14981
14997
  ]
14998
+ },
14999
+ // v42 -> v43: Give the conversation list reads an index they can actually
15000
+ // sort on. All five of them (getConversationsOp, getConversationsLazyOp,
15001
+ // getConversationsByProjectOp, getConversationsByProjectLazyOp and the
15002
+ // keyset getConversationsPageOp) emit `where is_deleted is 0 ... order by
15003
+ // created_at desc`, so before this they resolved is_deleted through its
15004
+ // index and then built a temp B-tree over every live row just to return
15005
+ // the newest page.
15006
+ //
15007
+ // The index is COMPOSITE, and that is the whole point — a bare index on
15008
+ // created_at does not fix this. SQLite will not combine two single-column
15009
+ // indexes here, so with `is_deleted` indexed and `created_at` indexed
15010
+ // separately it still picks conversations_is_deleted and still temp-sorts.
15011
+ // It only prefers a lone created_at index once ANALYZE has populated
15012
+ // sqlite_stat1, and nothing ever runs ANALYZE: not WatermelonDB, not its
15013
+ // native SQLite bindings, not this SDK. Every device is permanently in the
15014
+ // no-statistics state, so the index has to satisfy the filter and the sort
15015
+ // in one structure. `(is_deleted, created_at)` does: equality on the
15016
+ // leading column, ordered scan on the second, and the keyset boundary
15017
+ // becomes a range seek on the same index.
15018
+ //
15019
+ // unsafeExecuteSql, not addColumns: both columns already exist and
15020
+ // WatermelonDB has no add-index migration step, so raw SQL is the only way
15021
+ // to build one in place (same shape as the v19 -> v20 memory_vault
15022
+ // migration). IF NOT EXISTS keeps the step idempotent.
15023
+ //
15024
+ // Two limits worth knowing before someone re-measures this and finds it
15025
+ // missing. First, the LokiJS (web) adapter discards `sql` steps entirely,
15026
+ // so existing browser databases gain nothing — web's only lever is the
15027
+ // `isIndexed` flag on the column itself. Second, this index reaches
15028
+ // MIGRATED databases only: WatermelonDB builds a fresh database purely from
15029
+ // the encoded schema, and its schema format cannot express a composite
15030
+ // index. The one hook that could (`unsafeSql` on the table) is a function,
15031
+ // and the LokiJS adapter posts the schema to its worker, so attaching it
15032
+ // makes the whole schema fail structuredClone and takes the database down
15033
+ // at setup for anyone on `useWebWorker: true`. Not worth it for an index.
15034
+ {
15035
+ toVersion: 43,
15036
+ steps: [
15037
+ unsafeExecuteSql(
15038
+ "CREATE INDEX IF NOT EXISTS conversations_is_deleted_created_at ON conversations (is_deleted, created_at);"
15039
+ )
15040
+ ]
14982
15041
  }
14983
15042
  ]
14984
15043
  });
@@ -55,12 +55,14 @@ async function consolidateMemory(newContent, candidates, options) {
55
55
  if (trimmed.length === 0) return fallback;
56
56
  if (candidates.length === 0) return fallback;
57
57
  const redactor = resolvePiiRedactor(options.piiRedaction);
58
- const safeTrimmed = redactor ? redactor.redactText(trimmed).text : trimmed;
59
- const candidateText = candidates.map((c, i) => {
60
- const safeContent = redactor ? redactor.redactText(c.content).text : c.content;
61
- return `[${i + 1}] (id: ${c.id}, sim: ${c.similarity.toFixed(2)})
62
- ${safeContent}`;
63
- }).join("\n");
58
+ const safeTrimmed = redactor ? (await redactor.redactTextAsync(trimmed)).text : trimmed;
59
+ const rows = [];
60
+ for (const [i, c] of candidates.entries()) {
61
+ const safeContent = redactor ? (await redactor.redactTextAsync(c.content)).text : c.content;
62
+ rows.push(`[${i + 1}] (id: ${c.id}, sim: ${c.similarity.toFixed(2)})
63
+ ${safeContent}`);
64
+ }
65
+ const candidateText = rows.join("\n");
64
66
  const userMessage = `New memory:
65
67
  ${safeTrimmed}
66
68
 
@@ -23580,12 +23580,14 @@ async function consolidateMemory(newContent, candidates, options) {
23580
23580
  if (trimmed.length === 0) return fallback;
23581
23581
  if (candidates.length === 0) return fallback;
23582
23582
  const redactor = resolvePiiRedactor(options.piiRedaction);
23583
- const safeTrimmed = redactor ? redactor.redactText(trimmed).text : trimmed;
23584
- const candidateText = candidates.map((c, i) => {
23585
- const safeContent = redactor ? redactor.redactText(c.content).text : c.content;
23586
- return `[${i + 1}] (id: ${c.id}, sim: ${c.similarity.toFixed(2)})
23587
- ${safeContent}`;
23588
- }).join("\n");
23583
+ const safeTrimmed = redactor ? (await redactor.redactTextAsync(trimmed)).text : trimmed;
23584
+ const rows = [];
23585
+ for (const [i, c] of candidates.entries()) {
23586
+ const safeContent = redactor ? (await redactor.redactTextAsync(c.content)).text : c.content;
23587
+ rows.push(`[${i + 1}] (id: ${c.id}, sim: ${c.similarity.toFixed(2)})
23588
+ ${safeContent}`);
23589
+ }
23590
+ const candidateText = rows.join("\n");
23589
23591
  const userMessage = `New memory:
23590
23592
  ${safeTrimmed}
23591
23593
 
@@ -24525,7 +24527,7 @@ __decorateClass([
24525
24527
  ], VaultFolder.prototype, "context", 2);
24526
24528
 
24527
24529
  // src/lib/db/schema.ts
24528
- var SDK_SCHEMA_VERSION = 42;
24530
+ var SDK_SCHEMA_VERSION = 43;
24529
24531
  var sdkSchema = (0, import_watermelondb12.appSchema)({
24530
24532
  version: SDK_SCHEMA_VERSION,
24531
24533
  tables: [
@@ -24573,7 +24575,16 @@ var sdkSchema = (0, import_watermelondb12.appSchema)({
24573
24575
  { name: "conversation_id", type: "string", isIndexed: true },
24574
24576
  { name: "title", type: "string" },
24575
24577
  { name: "project_id", type: "string", isOptional: true, isIndexed: true },
24576
- { name: "created_at", type: "number" },
24578
+ // Indexed to match every other list-sorted created_at in this schema.
24579
+ // Note this single-column index is NOT what makes the conversation list
24580
+ // reads fast on native SQLite — they all filter is_deleted as well, and
24581
+ // SQLite will not combine the two indexes, so the composite created by
24582
+ // the v42 migration is what its planner actually uses. This declaration
24583
+ // still earns its keep on the other two adapters: LokiJS builds its
24584
+ // binary indices from `isIndexed` (and ignores `sql` migration steps
24585
+ // outright), and Postgres gathers statistics via autovacuum, so its
24586
+ // planner can use a single-column index the SQLite planner skips.
24587
+ { name: "created_at", type: "number", isIndexed: true },
24577
24588
  { name: "updated_at", type: "number" },
24578
24589
  { name: "is_deleted", type: "boolean", isIndexed: true },
24579
24590
  { name: "pinned_at", type: "number", isOptional: true }
@@ -25440,6 +25451,49 @@ var sdkMigrations = (0, import_migrations.schemaMigrations)({
25440
25451
  ]
25441
25452
  })
25442
25453
  ]
25454
+ },
25455
+ // v42 -> v43: Give the conversation list reads an index they can actually
25456
+ // sort on. All five of them (getConversationsOp, getConversationsLazyOp,
25457
+ // getConversationsByProjectOp, getConversationsByProjectLazyOp and the
25458
+ // keyset getConversationsPageOp) emit `where is_deleted is 0 ... order by
25459
+ // created_at desc`, so before this they resolved is_deleted through its
25460
+ // index and then built a temp B-tree over every live row just to return
25461
+ // the newest page.
25462
+ //
25463
+ // The index is COMPOSITE, and that is the whole point — a bare index on
25464
+ // created_at does not fix this. SQLite will not combine two single-column
25465
+ // indexes here, so with `is_deleted` indexed and `created_at` indexed
25466
+ // separately it still picks conversations_is_deleted and still temp-sorts.
25467
+ // It only prefers a lone created_at index once ANALYZE has populated
25468
+ // sqlite_stat1, and nothing ever runs ANALYZE: not WatermelonDB, not its
25469
+ // native SQLite bindings, not this SDK. Every device is permanently in the
25470
+ // no-statistics state, so the index has to satisfy the filter and the sort
25471
+ // in one structure. `(is_deleted, created_at)` does: equality on the
25472
+ // leading column, ordered scan on the second, and the keyset boundary
25473
+ // becomes a range seek on the same index.
25474
+ //
25475
+ // unsafeExecuteSql, not addColumns: both columns already exist and
25476
+ // WatermelonDB has no add-index migration step, so raw SQL is the only way
25477
+ // to build one in place (same shape as the v19 -> v20 memory_vault
25478
+ // migration). IF NOT EXISTS keeps the step idempotent.
25479
+ //
25480
+ // Two limits worth knowing before someone re-measures this and finds it
25481
+ // missing. First, the LokiJS (web) adapter discards `sql` steps entirely,
25482
+ // so existing browser databases gain nothing — web's only lever is the
25483
+ // `isIndexed` flag on the column itself. Second, this index reaches
25484
+ // MIGRATED databases only: WatermelonDB builds a fresh database purely from
25485
+ // the encoded schema, and its schema format cannot express a composite
25486
+ // index. The one hook that could (`unsafeSql` on the table) is a function,
25487
+ // and the LokiJS adapter posts the schema to its worker, so attaching it
25488
+ // makes the whole schema fail structuredClone and takes the database down
25489
+ // at setup for anyone on `useWebWorker: true`. Not worth it for an index.
25490
+ {
25491
+ toVersion: 43,
25492
+ steps: [
25493
+ (0, import_migrations.unsafeExecuteSql)(
25494
+ "CREATE INDEX IF NOT EXISTS conversations_is_deleted_created_at ON conversations (is_deleted, created_at);"
25495
+ )
25496
+ ]
25443
25497
  }
25444
25498
  ]
25445
25499
  });
@@ -28208,10 +28262,12 @@ async function classifyInjectionCandidates(candidates, options) {
28208
28262
  );
28209
28263
  }
28210
28264
  const redactor = resolvePiiRedactor(options.piiRedaction);
28211
- const numbered = scope.map((c, i) => {
28212
- const safe = redactor ? redactor.redactText(c.content).text : c.content;
28213
- return `[${i + 1}] ${safe}`;
28214
- }).join("\n");
28265
+ const lines = [];
28266
+ for (const [i, c] of scope.entries()) {
28267
+ const safe = redactor ? (await redactor.redactTextAsync(c.content)).text : c.content;
28268
+ lines.push(`[${i + 1}] ${safe}`);
28269
+ }
28270
+ const numbered = lines.join("\n");
28215
28271
  let parsed;
28216
28272
  try {
28217
28273
  parsed = await callPortalJsonCompletion({
@@ -29533,7 +29589,7 @@ function createLlmDecayClassifier(options) {
29533
29589
  }
29534
29590
  if (!content || content.trim().length === 0) return ruleVerdict;
29535
29591
  const trimmed = content.trim().slice(0, MAX_CONTENT_CHARS);
29536
- const safe = redactor ? redactor.redactText(trimmed).text : trimmed;
29592
+ const safe = redactor ? (await redactor.redactTextAsync(trimmed)).text : trimmed;
29537
29593
  const meta = `factType: ${input.factType ?? "none"}; ageDays: ${ageDays(input.updatedAt, now)}`;
29538
29594
  let parsed;
29539
29595
  try {
@@ -30663,17 +30719,22 @@ async function extractEntitiesForMemories(memories, options) {
30663
30719
  const out = /* @__PURE__ */ new Map();
30664
30720
  if (memories.length === 0) return out;
30665
30721
  const redactor = resolvePiiRedactor(options.piiRedaction);
30666
- const vocabulary = (options.existingEntityNames ?? []).filter((n) => n.trim().length > 0).slice(0, MAX_VOCABULARY_NAMES).map((n) => redactor ? redactor.redactText(n).text : n);
30722
+ const vocabulary = [];
30723
+ for (const name of (options.existingEntityNames ?? []).filter((n) => n.trim().length > 0).slice(0, MAX_VOCABULARY_NAMES)) {
30724
+ vocabulary.push(redactor ? (await redactor.redactTextAsync(name)).text : name);
30725
+ }
30667
30726
  const vocabularyNote = vocabulary.length > 0 ? `The user's existing topics: ${vocabulary.join(", ")}.
30668
30727
  When a memory refers to one of these, reuse the EXACT existing name (same spelling, casing, spacing and punctuation) instead of a variant \u2014 if "Foo.ai" is listed, never emit "Foo ai" or "Foo AI", and if "Jane Roe" is listed, emit that rather than a bare "Jane".
30669
30728
 
30670
30729
  ` : "";
30671
30730
  for (let i = 0; i < memories.length; i += TOPIC_EXTRACTION_BATCH_SIZE) {
30672
30731
  const batch = memories.slice(i, i + TOPIC_EXTRACTION_BATCH_SIZE);
30673
- const listing = batch.map((m) => {
30732
+ const rows = [];
30733
+ for (const m of batch) {
30674
30734
  const content = m.content.slice(0, MAX_CHARS_PER_MEMORY).replace(/\s+/g, " ").trim();
30675
- return `${m.id}: ${redactor ? redactor.redactText(content).text : content}`;
30676
- }).join("\n");
30735
+ rows.push(`${m.id}: ${redactor ? (await redactor.redactTextAsync(content)).text : content}`);
30736
+ }
30737
+ const listing = rows.join("\n");
30677
30738
  const parsed = await callPortalJsonCompletion({
30678
30739
  ...options.apiKey !== void 0 && { apiKey: options.apiKey },
30679
30740
  ...options.getToken !== void 0 && { getToken: options.getToken },
@@ -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;
@@ -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;
@@ -1319,7 +1319,7 @@ __decorateClass([
1319
1319
  ], VaultFolder.prototype, "context", 2);
1320
1320
 
1321
1321
  // src/lib/db/schema.ts
1322
- var SDK_SCHEMA_VERSION = 42;
1322
+ var SDK_SCHEMA_VERSION = 43;
1323
1323
  var sdkSchema = (0, import_watermelondb12.appSchema)({
1324
1324
  version: SDK_SCHEMA_VERSION,
1325
1325
  tables: [
@@ -1367,7 +1367,16 @@ var sdkSchema = (0, import_watermelondb12.appSchema)({
1367
1367
  { name: "conversation_id", type: "string", isIndexed: true },
1368
1368
  { name: "title", type: "string" },
1369
1369
  { name: "project_id", type: "string", isOptional: true, isIndexed: true },
1370
- { 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 },
1371
1380
  { name: "updated_at", type: "number" },
1372
1381
  { name: "is_deleted", type: "boolean", isIndexed: true },
1373
1382
  { name: "pinned_at", type: "number", isOptional: true }
@@ -2234,6 +2243,49 @@ var sdkMigrations = (0, import_migrations.schemaMigrations)({
2234
2243
  ]
2235
2244
  })
2236
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
+ ]
2237
2289
  }
2238
2290
  ]
2239
2291
  });
@@ -4965,10 +5017,12 @@ async function classifyInjectionCandidates(candidates, options) {
4965
5017
  );
4966
5018
  }
4967
5019
  const redactor = resolvePiiRedactor(options.piiRedaction);
4968
- const numbered = scope.map((c, i) => {
4969
- const safe = redactor ? redactor.redactText(c.content).text : c.content;
4970
- return `[${i + 1}] ${safe}`;
4971
- }).join("\n");
5020
+ const lines = [];
5021
+ for (const [i, c] of scope.entries()) {
5022
+ const safe = redactor ? (await redactor.redactTextAsync(c.content)).text : c.content;
5023
+ lines.push(`[${i + 1}] ${safe}`);
5024
+ }
5025
+ const numbered = lines.join("\n");
4972
5026
  let parsed;
4973
5027
  try {
4974
5028
  parsed = await callPortalJsonCompletion({
@@ -5449,7 +5503,7 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
5449
5503
  content: m.content,
5450
5504
  similarity: m.similarity
5451
5505
  }));
5452
- const { consolidateMemory: doConsolidate } = await import("./consolidate-GRMEDWKK.mjs");
5506
+ const { consolidateMemory: doConsolidate } = await import("./consolidate-LMIDJFR6.mjs");
5453
5507
  const decision = await doConsolidate(trimmed, candidates, consolidateOptions);
5454
5508
  if (decision.action === "create") return null;
5455
5509
  if (decision.action === "supersede" && decision.content) {
@@ -6273,7 +6327,7 @@ function createLlmDecayClassifier(options) {
6273
6327
  }
6274
6328
  if (!content || content.trim().length === 0) return ruleVerdict;
6275
6329
  const trimmed = content.trim().slice(0, MAX_CONTENT_CHARS);
6276
- const safe = redactor ? redactor.redactText(trimmed).text : trimmed;
6330
+ const safe = redactor ? (await redactor.redactTextAsync(trimmed)).text : trimmed;
6277
6331
  const meta = `factType: ${input.factType ?? "none"}; ageDays: ${ageDays(input.updatedAt, now)}`;
6278
6332
  let parsed;
6279
6333
  try {
@@ -7370,17 +7424,22 @@ async function extractEntitiesForMemories(memories, options) {
7370
7424
  const out = /* @__PURE__ */ new Map();
7371
7425
  if (memories.length === 0) return out;
7372
7426
  const redactor = resolvePiiRedactor(options.piiRedaction);
7373
- const vocabulary = (options.existingEntityNames ?? []).filter((n) => n.trim().length > 0).slice(0, MAX_VOCABULARY_NAMES).map((n) => redactor ? redactor.redactText(n).text : n);
7427
+ const vocabulary = [];
7428
+ for (const name of (options.existingEntityNames ?? []).filter((n) => n.trim().length > 0).slice(0, MAX_VOCABULARY_NAMES)) {
7429
+ vocabulary.push(redactor ? (await redactor.redactTextAsync(name)).text : name);
7430
+ }
7374
7431
  const vocabularyNote = vocabulary.length > 0 ? `The user's existing topics: ${vocabulary.join(", ")}.
7375
7432
  When a memory refers to one of these, reuse the EXACT existing name (same spelling, casing, spacing and punctuation) instead of a variant \u2014 if "Foo.ai" is listed, never emit "Foo ai" or "Foo AI", and if "Jane Roe" is listed, emit that rather than a bare "Jane".
7376
7433
 
7377
7434
  ` : "";
7378
7435
  for (let i = 0; i < memories.length; i += TOPIC_EXTRACTION_BATCH_SIZE) {
7379
7436
  const batch = memories.slice(i, i + TOPIC_EXTRACTION_BATCH_SIZE);
7380
- const listing = batch.map((m) => {
7437
+ const rows = [];
7438
+ for (const m of batch) {
7381
7439
  const content = m.content.slice(0, MAX_CHARS_PER_MEMORY).replace(/\s+/g, " ").trim();
7382
- return `${m.id}: ${redactor ? redactor.redactText(content).text : content}`;
7383
- }).join("\n");
7440
+ rows.push(`${m.id}: ${redactor ? (await redactor.redactTextAsync(content)).text : content}`);
7441
+ }
7442
+ const listing = rows.join("\n");
7384
7443
  const parsed = await callPortalJsonCompletion({
7385
7444
  ...options.apiKey !== void 0 && { apiKey: options.apiKey },
7386
7445
  ...options.getToken !== void 0 && { getToken: options.getToken },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anuma/sdk",
3
- "version": "5.0.0",
3
+ "version": "5.0.2",
4
4
  "description": "",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -136,8 +136,9 @@
136
136
  "lint:fix": "eslint . --fix",
137
137
  "format": "prettier --write .",
138
138
  "format:check": "prettier --check .",
139
- "typecheck": "tsc --noEmit && pnpm run typecheck:test",
139
+ "typecheck": "tsc --noEmit && pnpm run typecheck:test && pnpm run check:excluded-suites",
140
140
  "typecheck:test": "tsc -p tsconfig.test.json",
141
+ "check:excluded-suites": "node scripts/assert-excluded-suites-still-fail.mjs",
141
142
  "knip": "knip",
142
143
  "check:export-consumers": "node scripts/assert-export-consumers.mjs",
143
144
  "check-exports": "publint && attw --pack --ignore-rules no-resolution false-cjs && node scripts/assert-no-client-self-import.mjs",