@anuma/sdk 10.5.2 → 10.5.4

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.
@@ -4879,6 +4879,63 @@ async function chunkRowSource(ctx, filter) {
4879
4879
  return fullScanChunkRowSource(ctx, filter);
4880
4880
  }
4881
4881
  }
4882
+ async function readChunkVectorRow(ctx, row, onUnreadable) {
4883
+ const version = Number(row.updated_at) || 0;
4884
+ const chunks3 = await readRawJsonField(row, "chunks", ctx.walletAddress);
4885
+ if (chunks3 && chunks3.length > 0) {
4886
+ return {
4887
+ entry: {
4888
+ version,
4889
+ chunks: chunks3.map((chunk) => decodeChunkVector(chunk.vector, onUnreadable))
4890
+ },
4891
+ chunks: chunks3
4892
+ };
4893
+ }
4894
+ const messageVector = await readRawJsonField(row, "vector", ctx.walletAddress);
4895
+ if (!messageVector || messageVector.length === 0) return null;
4896
+ return { entry: { version, chunks: [], fallback: Float32Array.from(messageVector) }, chunks: [] };
4897
+ }
4898
+ async function warmChunkVectorCacheOp(ctx, options) {
4899
+ const { chunkCache, embeddingModel, limit, signal } = options;
4900
+ const source = await chunkRowSource(ctx, {});
4901
+ const yieldIfDue = createTimeSliceYielder(CHUNK_SCAN_YIELD_BUDGET_MS);
4902
+ let keys = source.keys.filter(
4903
+ (key3) => !embeddingModel || key3.embeddingModel === null || key3.embeddingModel === embeddingModel
4904
+ );
4905
+ if (limit !== void 0 && keys.length > limit) {
4906
+ keys = keys.sort((a, b) => b.version - a.version).slice(0, limit);
4907
+ }
4908
+ const missIds = [];
4909
+ for (const key3 of keys) {
4910
+ await yieldIfDue();
4911
+ if (chunkCache.get(key3.id)?.version !== key3.version) missIds.push(key3.id);
4912
+ }
4913
+ let warmed = 0;
4914
+ let unreadableVectors = 0;
4915
+ for await (const row of loadVectorRowsInBatches(source, missIds)) {
4916
+ await yieldIfDue();
4917
+ const read = await readChunkVectorRow(ctx, row, () => {
4918
+ unreadableVectors++;
4919
+ });
4920
+ if (signal?.aborted) break;
4921
+ if (!read) continue;
4922
+ chunkCache.set(row.id, read.entry);
4923
+ warmed++;
4924
+ }
4925
+ warnUnreadableChunkVectors("warmChunkVectorCacheOp", unreadableVectors);
4926
+ return warmed;
4927
+ }
4928
+ async function* loadVectorRowsInBatches(source, ids) {
4929
+ for (let offset = 0; offset < ids.length; offset += CHUNK_VECTOR_BATCH_SIZE) {
4930
+ yield* await source.loadVectorRows(ids.slice(offset, offset + CHUNK_VECTOR_BATCH_SIZE));
4931
+ }
4932
+ }
4933
+ function warnUnreadableChunkVectors(operation, count2) {
4934
+ if (count2 === 0) return;
4935
+ getLogger().warn(
4936
+ `${operation}: could not read ${count2} chunk vectors and left them out of ranking \u2014 the stored value is corrupt, so those chunks stay unsearchable until the message is re-embedded via chunkAndEmbedAllMessages`
4937
+ );
4938
+ }
4882
4939
  async function searchChunksOp(ctx, queryVector, options) {
4883
4940
  const {
4884
4941
  limit = 10,
@@ -4947,45 +5004,39 @@ async function searchChunksOp(ctx, queryVector, options) {
4947
5004
  for (const row of rows) {
4948
5005
  await yieldIfDue();
4949
5006
  const id2 = row.id;
4950
- const version = Number(row.updated_at) || 0;
4951
5007
  const order = orderById.get(id2) ?? Number.MAX_SAFE_INTEGER;
4952
- const chunks3 = await readRawJsonField(row, "chunks", ctx.walletAddress);
4953
- if (chunks3 && chunks3.length > 0) {
4954
- const vectors = chunks3.map(
4955
- (chunk) => decodeChunkVector(chunk.vector, () => {
4956
- unreadableVectors++;
4957
- })
4958
- );
4959
- chunkCache?.set(id2, { version, chunks: vectors });
4960
- for (let ci = 0; ci < chunks3.length; ci++) {
4961
- const vec = vectors[ci];
4962
- if (vec.length === 0) continue;
4963
- const similarity = cosineSimilarity(queryVector, vec);
4964
- if (similarity >= minSimilarity) {
4965
- candidates.push({
4966
- id: id2,
4967
- version,
4968
- order,
4969
- similarity,
4970
- chunkTextSource: { kind: "chunk", chunk: chunks3[ci], siblings: chunks3 }
4971
- });
4972
- }
5008
+ const read = await readChunkVectorRow(ctx, row, () => {
5009
+ unreadableVectors++;
5010
+ });
5011
+ if (!read) continue;
5012
+ const { entry, chunks: chunks3 } = read;
5013
+ const { version } = entry;
5014
+ chunkCache?.set(id2, entry);
5015
+ if (entry.fallback) {
5016
+ const similarity = cosineSimilarity(queryVector, entry.fallback);
5017
+ if (similarity >= minSimilarity) {
5018
+ candidates.push({
5019
+ id: id2,
5020
+ version,
5021
+ order,
5022
+ similarity,
5023
+ chunkTextSource: { kind: "message" }
5024
+ });
4973
5025
  }
4974
- } else {
4975
- const messageVector = await readRawJsonField(row, "vector", ctx.walletAddress);
4976
- if (messageVector && messageVector.length > 0) {
4977
- const fallback = Float32Array.from(messageVector);
4978
- chunkCache?.set(id2, { version, chunks: [], fallback });
4979
- const similarity = cosineSimilarity(queryVector, fallback);
4980
- if (similarity >= minSimilarity) {
4981
- candidates.push({
4982
- id: id2,
4983
- version,
4984
- order,
4985
- similarity,
4986
- chunkTextSource: { kind: "message" }
4987
- });
4988
- }
5026
+ continue;
5027
+ }
5028
+ for (let ci = 0; ci < chunks3.length; ci++) {
5029
+ const vec = entry.chunks[ci];
5030
+ if (vec.length === 0) continue;
5031
+ const similarity = cosineSimilarity(queryVector, vec);
5032
+ if (similarity >= minSimilarity) {
5033
+ candidates.push({
5034
+ id: id2,
5035
+ version,
5036
+ order,
5037
+ similarity,
5038
+ chunkTextSource: { kind: "chunk", chunk: chunks3[ci], siblings: chunks3 }
5039
+ });
4989
5040
  }
4990
5041
  }
4991
5042
  }
@@ -4996,11 +5047,7 @@ async function searchChunksOp(ctx, queryVector, options) {
4996
5047
  `searchChunksOp: skipped ${staleSkipped} messages whose embedding model differs from the current model (${embeddingModel}) \u2014 re-embed via chunkAndEmbedAllMessages`
4997
5048
  );
4998
5049
  }
4999
- if (unreadableVectors > 0) {
5000
- getLogger().warn(
5001
- `searchChunksOp: could not read ${unreadableVectors} chunk vectors and left them out of ranking \u2014 the stored value is corrupt, so those chunks stay unsearchable until the message is re-embedded via chunkAndEmbedAllMessages`
5002
- );
5003
- }
5050
+ warnUnreadableChunkVectors("searchChunksOp", unreadableVectors);
5004
5051
  const topK = candidates.sort((a, b) => b.similarity - a.similarity || a.order - b.order).slice(0, limit);
5005
5052
  const topIds = Array.from(new Set(topK.map((c) => c.id)));
5006
5053
  const fullRows = await source.loadFullRows(topIds);
@@ -12401,7 +12448,7 @@ async function createRemoteMemoryPersistence(options) {
12401
12448
  ...item,
12402
12449
  memory: {
12403
12450
  ...item.memory,
12404
- content: await decrypt(item.memory.content),
12451
+ content: item.memory.is_deleted && item.memory.content === "" ? "" : await decrypt(item.memory.content),
12405
12452
  ...item.memory.kind_value !== void 0 && {
12406
12453
  kind_value: await decrypt(item.memory.kind_value)
12407
12454
  }
@@ -12495,7 +12542,8 @@ async function createRemoteMemoryPersistence(options) {
12495
12542
  }
12496
12543
  if (existing && existing.version !== expectedVersion) throw conflict();
12497
12544
  const decoded = existing ? await decode(existing).catch(() => void 0) : void 0;
12498
- const content = (snapshot?.content ?? decoded?.memory.content) === memory.content ? snapshot?.encryptedContent ?? existing.memory.content : await encryptField2(memory.content);
12545
+ const reusableContent = snapshot?.encryptedContent ?? existing?.memory.content;
12546
+ const content = reusableContent && (snapshot?.content ?? decoded?.memory.content) === memory.content ? reusableContent : await encryptField2(memory.content);
12499
12547
  const kindValue = memory.kind_value === void 0 ? void 0 : (snapshot?.kindValue ?? decoded?.memory.kind_value) === memory.kind_value ? snapshot?.encryptedKindValue ?? existing.memory.kind_value : await encryptField2(memory.kind_value);
12500
12548
  return {
12501
12549
  body: {
@@ -25812,6 +25860,13 @@ var METADATA_FIELDS = [
25812
25860
  "published_at",
25813
25861
  "geohash"
25814
25862
  ];
25863
+ var TOMBSTONE_FIELDS = [
25864
+ "scope",
25865
+ "is_deleted",
25866
+ "embedding_model",
25867
+ "topics_updated_at",
25868
+ "last_observed_at"
25869
+ ];
25815
25870
  function text15(value) {
25816
25871
  return typeof value === "string" && value !== "" ? value : void 0;
25817
25872
  }
@@ -25959,7 +26014,8 @@ async function embedMissing(prepared, options, model, signal) {
25959
26014
  }
25960
26015
  function sameMetadata2(local, remote, model) {
25961
26016
  const mine = metadata2(local, model);
25962
- return METADATA_FIELDS.every((field10) => {
26017
+ const fields = bool(local.is_deleted) ? TOMBSTONE_FIELDS : METADATA_FIELDS;
26018
+ return fields.every((field10) => {
25963
26019
  const a = mine[field10] ?? null;
25964
26020
  const b = remote[field10] ?? null;
25965
26021
  if (field10 === "embedding_model" && a === null) return true;
@@ -29907,6 +29963,9 @@ var WatermelonChatStorageAdapter = class {
29907
29963
  searchChunks(queryVector, options) {
29908
29964
  return searchChunksOp(this.ctx, queryVector, options);
29909
29965
  }
29966
+ warmChunkCache(options) {
29967
+ return warmChunkVectorCacheOp(this.ctx, options);
29968
+ }
29910
29969
  getSummary(conversationId) {
29911
29970
  return getConversationSummaryOp(
29912
29971
  createSummaryContext(this.ctx.database, this.ctx),
@@ -4436,6 +4436,13 @@ interface ChatBackend extends ChatStorageAdapter {
4436
4436
  uniqueId: string;
4437
4437
  }): Promise<StoredMessage>;
4438
4438
  searchChunks(queryVector: number[], options?: ChatChunkSearchOptions): Promise<ChunkSearchResult[]>;
4439
+ /** Fills a chunk vector cache ahead of the first search, for backends that score on the device. */
4440
+ warmChunkCache?(options: {
4441
+ chunkCache: ChunkVectorCache;
4442
+ embeddingModel?: string;
4443
+ limit?: number;
4444
+ signal?: AbortSignal;
4445
+ }): Promise<number>;
4439
4446
  getSummary(conversationId: string): Promise<StoredConversationSummary | null>;
4440
4447
  upsertSummary(conversationId: string, summary: string, summarizedUpTo: string, tokenCount: number): Promise<StoredConversationSummary>;
4441
4448
  deleteSummary(conversationId: string): Promise<void>;
@@ -4436,6 +4436,13 @@ interface ChatBackend extends ChatStorageAdapter {
4436
4436
  uniqueId: string;
4437
4437
  }): Promise<StoredMessage>;
4438
4438
  searchChunks(queryVector: number[], options?: ChatChunkSearchOptions): Promise<ChunkSearchResult[]>;
4439
+ /** Fills a chunk vector cache ahead of the first search, for backends that score on the device. */
4440
+ warmChunkCache?(options: {
4441
+ chunkCache: ChunkVectorCache;
4442
+ embeddingModel?: string;
4443
+ limit?: number;
4444
+ signal?: AbortSignal;
4445
+ }): Promise<number>;
4439
4446
  getSummary(conversationId: string): Promise<StoredConversationSummary | null>;
4440
4447
  upsertSummary(conversationId: string, summary: string, summarizedUpTo: string, tokenCount: number): Promise<StoredConversationSummary>;
4441
4448
  deleteSummary(conversationId: string): Promise<void>;
@@ -3234,6 +3234,63 @@ async function chunkRowSource(ctx, filter) {
3234
3234
  return fullScanChunkRowSource(ctx, filter);
3235
3235
  }
3236
3236
  }
3237
+ async function readChunkVectorRow(ctx, row, onUnreadable) {
3238
+ const version = Number(row.updated_at) || 0;
3239
+ const chunks3 = await readRawJsonField(row, "chunks", ctx.walletAddress);
3240
+ if (chunks3 && chunks3.length > 0) {
3241
+ return {
3242
+ entry: {
3243
+ version,
3244
+ chunks: chunks3.map((chunk) => decodeChunkVector(chunk.vector, onUnreadable))
3245
+ },
3246
+ chunks: chunks3
3247
+ };
3248
+ }
3249
+ const messageVector = await readRawJsonField(row, "vector", ctx.walletAddress);
3250
+ if (!messageVector || messageVector.length === 0) return null;
3251
+ return { entry: { version, chunks: [], fallback: Float32Array.from(messageVector) }, chunks: [] };
3252
+ }
3253
+ async function warmChunkVectorCacheOp(ctx, options) {
3254
+ const { chunkCache, embeddingModel, limit, signal } = options;
3255
+ const source = await chunkRowSource(ctx, {});
3256
+ const yieldIfDue = createTimeSliceYielder(CHUNK_SCAN_YIELD_BUDGET_MS);
3257
+ let keys = source.keys.filter(
3258
+ (key3) => !embeddingModel || key3.embeddingModel === null || key3.embeddingModel === embeddingModel
3259
+ );
3260
+ if (limit !== void 0 && keys.length > limit) {
3261
+ keys = keys.sort((a, b) => b.version - a.version).slice(0, limit);
3262
+ }
3263
+ const missIds = [];
3264
+ for (const key3 of keys) {
3265
+ await yieldIfDue();
3266
+ if (chunkCache.get(key3.id)?.version !== key3.version) missIds.push(key3.id);
3267
+ }
3268
+ let warmed = 0;
3269
+ let unreadableVectors = 0;
3270
+ for await (const row of loadVectorRowsInBatches(source, missIds)) {
3271
+ await yieldIfDue();
3272
+ const read = await readChunkVectorRow(ctx, row, () => {
3273
+ unreadableVectors++;
3274
+ });
3275
+ if (signal?.aborted) break;
3276
+ if (!read) continue;
3277
+ chunkCache.set(row.id, read.entry);
3278
+ warmed++;
3279
+ }
3280
+ warnUnreadableChunkVectors("warmChunkVectorCacheOp", unreadableVectors);
3281
+ return warmed;
3282
+ }
3283
+ async function* loadVectorRowsInBatches(source, ids) {
3284
+ for (let offset = 0; offset < ids.length; offset += CHUNK_VECTOR_BATCH_SIZE) {
3285
+ yield* await source.loadVectorRows(ids.slice(offset, offset + CHUNK_VECTOR_BATCH_SIZE));
3286
+ }
3287
+ }
3288
+ function warnUnreadableChunkVectors(operation, count2) {
3289
+ if (count2 === 0) return;
3290
+ getLogger().warn(
3291
+ `${operation}: could not read ${count2} chunk vectors and left them out of ranking \u2014 the stored value is corrupt, so those chunks stay unsearchable until the message is re-embedded via chunkAndEmbedAllMessages`
3292
+ );
3293
+ }
3237
3294
  async function searchChunksOp(ctx, queryVector, options) {
3238
3295
  const {
3239
3296
  limit = 10,
@@ -3302,45 +3359,39 @@ async function searchChunksOp(ctx, queryVector, options) {
3302
3359
  for (const row of rows) {
3303
3360
  await yieldIfDue();
3304
3361
  const id2 = row.id;
3305
- const version = Number(row.updated_at) || 0;
3306
3362
  const order = orderById.get(id2) ?? Number.MAX_SAFE_INTEGER;
3307
- const chunks3 = await readRawJsonField(row, "chunks", ctx.walletAddress);
3308
- if (chunks3 && chunks3.length > 0) {
3309
- const vectors = chunks3.map(
3310
- (chunk) => decodeChunkVector(chunk.vector, () => {
3311
- unreadableVectors++;
3312
- })
3313
- );
3314
- chunkCache?.set(id2, { version, chunks: vectors });
3315
- for (let ci = 0; ci < chunks3.length; ci++) {
3316
- const vec = vectors[ci];
3317
- if (vec.length === 0) continue;
3318
- const similarity = cosineSimilarity(queryVector, vec);
3319
- if (similarity >= minSimilarity) {
3320
- candidates.push({
3321
- id: id2,
3322
- version,
3323
- order,
3324
- similarity,
3325
- chunkTextSource: { kind: "chunk", chunk: chunks3[ci], siblings: chunks3 }
3326
- });
3327
- }
3363
+ const read = await readChunkVectorRow(ctx, row, () => {
3364
+ unreadableVectors++;
3365
+ });
3366
+ if (!read) continue;
3367
+ const { entry, chunks: chunks3 } = read;
3368
+ const { version } = entry;
3369
+ chunkCache?.set(id2, entry);
3370
+ if (entry.fallback) {
3371
+ const similarity = cosineSimilarity(queryVector, entry.fallback);
3372
+ if (similarity >= minSimilarity) {
3373
+ candidates.push({
3374
+ id: id2,
3375
+ version,
3376
+ order,
3377
+ similarity,
3378
+ chunkTextSource: { kind: "message" }
3379
+ });
3328
3380
  }
3329
- } else {
3330
- const messageVector = await readRawJsonField(row, "vector", ctx.walletAddress);
3331
- if (messageVector && messageVector.length > 0) {
3332
- const fallback = Float32Array.from(messageVector);
3333
- chunkCache?.set(id2, { version, chunks: [], fallback });
3334
- const similarity = cosineSimilarity(queryVector, fallback);
3335
- if (similarity >= minSimilarity) {
3336
- candidates.push({
3337
- id: id2,
3338
- version,
3339
- order,
3340
- similarity,
3341
- chunkTextSource: { kind: "message" }
3342
- });
3343
- }
3381
+ continue;
3382
+ }
3383
+ for (let ci = 0; ci < chunks3.length; ci++) {
3384
+ const vec = entry.chunks[ci];
3385
+ if (vec.length === 0) continue;
3386
+ const similarity = cosineSimilarity(queryVector, vec);
3387
+ if (similarity >= minSimilarity) {
3388
+ candidates.push({
3389
+ id: id2,
3390
+ version,
3391
+ order,
3392
+ similarity,
3393
+ chunkTextSource: { kind: "chunk", chunk: chunks3[ci], siblings: chunks3 }
3394
+ });
3344
3395
  }
3345
3396
  }
3346
3397
  }
@@ -3351,11 +3402,7 @@ async function searchChunksOp(ctx, queryVector, options) {
3351
3402
  `searchChunksOp: skipped ${staleSkipped} messages whose embedding model differs from the current model (${embeddingModel}) \u2014 re-embed via chunkAndEmbedAllMessages`
3352
3403
  );
3353
3404
  }
3354
- if (unreadableVectors > 0) {
3355
- getLogger().warn(
3356
- `searchChunksOp: could not read ${unreadableVectors} chunk vectors and left them out of ranking \u2014 the stored value is corrupt, so those chunks stay unsearchable until the message is re-embedded via chunkAndEmbedAllMessages`
3357
- );
3358
- }
3405
+ warnUnreadableChunkVectors("searchChunksOp", unreadableVectors);
3359
3406
  const topK = candidates.sort((a, b) => b.similarity - a.similarity || a.order - b.order).slice(0, limit);
3360
3407
  const topIds = Array.from(new Set(topK.map((c) => c.id)));
3361
3408
  const fullRows = await source.loadFullRows(topIds);
@@ -10729,7 +10776,7 @@ async function createRemoteMemoryPersistence(options) {
10729
10776
  ...item,
10730
10777
  memory: {
10731
10778
  ...item.memory,
10732
- content: await decrypt(item.memory.content),
10779
+ content: item.memory.is_deleted && item.memory.content === "" ? "" : await decrypt(item.memory.content),
10733
10780
  ...item.memory.kind_value !== void 0 && {
10734
10781
  kind_value: await decrypt(item.memory.kind_value)
10735
10782
  }
@@ -10823,7 +10870,8 @@ async function createRemoteMemoryPersistence(options) {
10823
10870
  }
10824
10871
  if (existing && existing.version !== expectedVersion) throw conflict();
10825
10872
  const decoded = existing ? await decode(existing).catch(() => void 0) : void 0;
10826
- const content = (snapshot?.content ?? decoded?.memory.content) === memory.content ? snapshot?.encryptedContent ?? existing.memory.content : await encryptField2(memory.content);
10873
+ const reusableContent = snapshot?.encryptedContent ?? existing?.memory.content;
10874
+ const content = reusableContent && (snapshot?.content ?? decoded?.memory.content) === memory.content ? reusableContent : await encryptField2(memory.content);
10827
10875
  const kindValue = memory.kind_value === void 0 ? void 0 : (snapshot?.kindValue ?? decoded?.memory.kind_value) === memory.kind_value ? snapshot?.encryptedKindValue ?? existing.memory.kind_value : await encryptField2(memory.kind_value);
10828
10876
  return {
10829
10877
  body: {
@@ -24093,6 +24141,13 @@ var METADATA_FIELDS = [
24093
24141
  "published_at",
24094
24142
  "geohash"
24095
24143
  ];
24144
+ var TOMBSTONE_FIELDS = [
24145
+ "scope",
24146
+ "is_deleted",
24147
+ "embedding_model",
24148
+ "topics_updated_at",
24149
+ "last_observed_at"
24150
+ ];
24096
24151
  function text15(value) {
24097
24152
  return typeof value === "string" && value !== "" ? value : void 0;
24098
24153
  }
@@ -24240,7 +24295,8 @@ async function embedMissing(prepared, options, model, signal) {
24240
24295
  }
24241
24296
  function sameMetadata2(local, remote, model) {
24242
24297
  const mine = metadata2(local, model);
24243
- return METADATA_FIELDS.every((field10) => {
24298
+ const fields = bool(local.is_deleted) ? TOMBSTONE_FIELDS : METADATA_FIELDS;
24299
+ return fields.every((field10) => {
24244
24300
  const a = mine[field10] ?? null;
24245
24301
  const b = remote[field10] ?? null;
24246
24302
  if (field10 === "embedding_model" && a === null) return true;
@@ -28178,6 +28234,9 @@ var WatermelonChatStorageAdapter = class {
28178
28234
  searchChunks(queryVector, options) {
28179
28235
  return searchChunksOp(this.ctx, queryVector, options);
28180
28236
  }
28237
+ warmChunkCache(options) {
28238
+ return warmChunkVectorCacheOp(this.ctx, options);
28239
+ }
28181
28240
  getSummary(conversationId) {
28182
28241
  return getConversationSummaryOp(
28183
28242
  createSummaryContext(this.ctx.database, this.ctx),
@@ -5619,6 +5619,63 @@ async function chunkRowSource(ctx, filter) {
5619
5619
  return fullScanChunkRowSource(ctx, filter);
5620
5620
  }
5621
5621
  }
5622
+ async function readChunkVectorRow(ctx, row, onUnreadable) {
5623
+ const version = Number(row.updated_at) || 0;
5624
+ const chunks3 = await readRawJsonField(row, "chunks", ctx.walletAddress);
5625
+ if (chunks3 && chunks3.length > 0) {
5626
+ return {
5627
+ entry: {
5628
+ version,
5629
+ chunks: chunks3.map((chunk) => decodeChunkVector(chunk.vector, onUnreadable))
5630
+ },
5631
+ chunks: chunks3
5632
+ };
5633
+ }
5634
+ const messageVector = await readRawJsonField(row, "vector", ctx.walletAddress);
5635
+ if (!messageVector || messageVector.length === 0) return null;
5636
+ return { entry: { version, chunks: [], fallback: Float32Array.from(messageVector) }, chunks: [] };
5637
+ }
5638
+ async function warmChunkVectorCacheOp(ctx, options) {
5639
+ const { chunkCache, embeddingModel, limit, signal } = options;
5640
+ const source = await chunkRowSource(ctx, {});
5641
+ const yieldIfDue = createTimeSliceYielder(CHUNK_SCAN_YIELD_BUDGET_MS);
5642
+ let keys = source.keys.filter(
5643
+ (key3) => !embeddingModel || key3.embeddingModel === null || key3.embeddingModel === embeddingModel
5644
+ );
5645
+ if (limit !== void 0 && keys.length > limit) {
5646
+ keys = keys.sort((a, b) => b.version - a.version).slice(0, limit);
5647
+ }
5648
+ const missIds = [];
5649
+ for (const key3 of keys) {
5650
+ await yieldIfDue();
5651
+ if (chunkCache.get(key3.id)?.version !== key3.version) missIds.push(key3.id);
5652
+ }
5653
+ let warmed = 0;
5654
+ let unreadableVectors = 0;
5655
+ for await (const row of loadVectorRowsInBatches(source, missIds)) {
5656
+ await yieldIfDue();
5657
+ const read = await readChunkVectorRow(ctx, row, () => {
5658
+ unreadableVectors++;
5659
+ });
5660
+ if (signal?.aborted) break;
5661
+ if (!read) continue;
5662
+ chunkCache.set(row.id, read.entry);
5663
+ warmed++;
5664
+ }
5665
+ warnUnreadableChunkVectors("warmChunkVectorCacheOp", unreadableVectors);
5666
+ return warmed;
5667
+ }
5668
+ async function* loadVectorRowsInBatches(source, ids) {
5669
+ for (let offset = 0; offset < ids.length; offset += CHUNK_VECTOR_BATCH_SIZE) {
5670
+ yield* await source.loadVectorRows(ids.slice(offset, offset + CHUNK_VECTOR_BATCH_SIZE));
5671
+ }
5672
+ }
5673
+ function warnUnreadableChunkVectors(operation, count2) {
5674
+ if (count2 === 0) return;
5675
+ getLogger().warn(
5676
+ `${operation}: could not read ${count2} chunk vectors and left them out of ranking \u2014 the stored value is corrupt, so those chunks stay unsearchable until the message is re-embedded via chunkAndEmbedAllMessages`
5677
+ );
5678
+ }
5622
5679
  async function searchChunksOp(ctx, queryVector, options) {
5623
5680
  const {
5624
5681
  limit = 10,
@@ -5687,45 +5744,39 @@ async function searchChunksOp(ctx, queryVector, options) {
5687
5744
  for (const row of rows) {
5688
5745
  await yieldIfDue();
5689
5746
  const id2 = row.id;
5690
- const version = Number(row.updated_at) || 0;
5691
5747
  const order = orderById.get(id2) ?? Number.MAX_SAFE_INTEGER;
5692
- const chunks3 = await readRawJsonField(row, "chunks", ctx.walletAddress);
5693
- if (chunks3 && chunks3.length > 0) {
5694
- const vectors = chunks3.map(
5695
- (chunk) => decodeChunkVector(chunk.vector, () => {
5696
- unreadableVectors++;
5697
- })
5698
- );
5699
- chunkCache?.set(id2, { version, chunks: vectors });
5700
- for (let ci = 0; ci < chunks3.length; ci++) {
5701
- const vec = vectors[ci];
5702
- if (vec.length === 0) continue;
5703
- const similarity = cosineSimilarity(queryVector, vec);
5704
- if (similarity >= minSimilarity) {
5705
- candidates.push({
5706
- id: id2,
5707
- version,
5708
- order,
5709
- similarity,
5710
- chunkTextSource: { kind: "chunk", chunk: chunks3[ci], siblings: chunks3 }
5711
- });
5712
- }
5748
+ const read = await readChunkVectorRow(ctx, row, () => {
5749
+ unreadableVectors++;
5750
+ });
5751
+ if (!read) continue;
5752
+ const { entry, chunks: chunks3 } = read;
5753
+ const { version } = entry;
5754
+ chunkCache?.set(id2, entry);
5755
+ if (entry.fallback) {
5756
+ const similarity = cosineSimilarity(queryVector, entry.fallback);
5757
+ if (similarity >= minSimilarity) {
5758
+ candidates.push({
5759
+ id: id2,
5760
+ version,
5761
+ order,
5762
+ similarity,
5763
+ chunkTextSource: { kind: "message" }
5764
+ });
5713
5765
  }
5714
- } else {
5715
- const messageVector = await readRawJsonField(row, "vector", ctx.walletAddress);
5716
- if (messageVector && messageVector.length > 0) {
5717
- const fallback = Float32Array.from(messageVector);
5718
- chunkCache?.set(id2, { version, chunks: [], fallback });
5719
- const similarity = cosineSimilarity(queryVector, fallback);
5720
- if (similarity >= minSimilarity) {
5721
- candidates.push({
5722
- id: id2,
5723
- version,
5724
- order,
5725
- similarity,
5726
- chunkTextSource: { kind: "message" }
5727
- });
5728
- }
5766
+ continue;
5767
+ }
5768
+ for (let ci = 0; ci < chunks3.length; ci++) {
5769
+ const vec = entry.chunks[ci];
5770
+ if (vec.length === 0) continue;
5771
+ const similarity = cosineSimilarity(queryVector, vec);
5772
+ if (similarity >= minSimilarity) {
5773
+ candidates.push({
5774
+ id: id2,
5775
+ version,
5776
+ order,
5777
+ similarity,
5778
+ chunkTextSource: { kind: "chunk", chunk: chunks3[ci], siblings: chunks3 }
5779
+ });
5729
5780
  }
5730
5781
  }
5731
5782
  }
@@ -5736,11 +5787,7 @@ async function searchChunksOp(ctx, queryVector, options) {
5736
5787
  `searchChunksOp: skipped ${staleSkipped} messages whose embedding model differs from the current model (${embeddingModel}) \u2014 re-embed via chunkAndEmbedAllMessages`
5737
5788
  );
5738
5789
  }
5739
- if (unreadableVectors > 0) {
5740
- getLogger().warn(
5741
- `searchChunksOp: could not read ${unreadableVectors} chunk vectors and left them out of ranking \u2014 the stored value is corrupt, so those chunks stay unsearchable until the message is re-embedded via chunkAndEmbedAllMessages`
5742
- );
5743
- }
5790
+ warnUnreadableChunkVectors("searchChunksOp", unreadableVectors);
5744
5791
  const topK = candidates.sort((a, b) => b.similarity - a.similarity || a.order - b.order).slice(0, limit);
5745
5792
  const topIds = Array.from(new Set(topK.map((c) => c.id)));
5746
5793
  const fullRows = await source.loadFullRows(topIds);
@@ -13171,7 +13218,7 @@ async function createRemoteMemoryPersistence(options) {
13171
13218
  ...item,
13172
13219
  memory: {
13173
13220
  ...item.memory,
13174
- content: await decrypt(item.memory.content),
13221
+ content: item.memory.is_deleted && item.memory.content === "" ? "" : await decrypt(item.memory.content),
13175
13222
  ...item.memory.kind_value !== void 0 && {
13176
13223
  kind_value: await decrypt(item.memory.kind_value)
13177
13224
  }
@@ -13265,7 +13312,8 @@ async function createRemoteMemoryPersistence(options) {
13265
13312
  }
13266
13313
  if (existing && existing.version !== expectedVersion) throw conflict();
13267
13314
  const decoded = existing ? await decode(existing).catch(() => void 0) : void 0;
13268
- const content = (snapshot?.content ?? decoded?.memory.content) === memory.content ? snapshot?.encryptedContent ?? existing.memory.content : await encryptField2(memory.content);
13315
+ const reusableContent = snapshot?.encryptedContent ?? existing?.memory.content;
13316
+ const content = reusableContent && (snapshot?.content ?? decoded?.memory.content) === memory.content ? reusableContent : await encryptField2(memory.content);
13269
13317
  const kindValue = memory.kind_value === void 0 ? void 0 : (snapshot?.kindValue ?? decoded?.memory.kind_value) === memory.kind_value ? snapshot?.encryptedKindValue ?? existing.memory.kind_value : await encryptField2(memory.kind_value);
13270
13318
  return {
13271
13319
  body: {
@@ -18392,6 +18440,9 @@ var WatermelonChatStorageAdapter = class {
18392
18440
  searchChunks(queryVector, options) {
18393
18441
  return searchChunksOp(this.ctx, queryVector, options);
18394
18442
  }
18443
+ warmChunkCache(options) {
18444
+ return warmChunkVectorCacheOp(this.ctx, options);
18445
+ }
18395
18446
  getSummary(conversationId) {
18396
18447
  return getConversationSummaryOp(
18397
18448
  createSummaryContext(this.ctx.database, this.ctx),
@@ -28142,6 +28193,13 @@ var METADATA_FIELDS = [
28142
28193
  "published_at",
28143
28194
  "geohash"
28144
28195
  ];
28196
+ var TOMBSTONE_FIELDS = [
28197
+ "scope",
28198
+ "is_deleted",
28199
+ "embedding_model",
28200
+ "topics_updated_at",
28201
+ "last_observed_at"
28202
+ ];
28145
28203
  function text15(value) {
28146
28204
  return typeof value === "string" && value !== "" ? value : void 0;
28147
28205
  }
@@ -28289,7 +28347,8 @@ async function embedMissing(prepared, options, model, signal) {
28289
28347
  }
28290
28348
  function sameMetadata2(local, remote, model) {
28291
28349
  const mine = metadata2(local, model);
28292
- return METADATA_FIELDS.every((field10) => {
28350
+ const fields = bool(local.is_deleted) ? TOMBSTONE_FIELDS : METADATA_FIELDS;
28351
+ return fields.every((field10) => {
28293
28352
  const a = mine[field10] ?? null;
28294
28353
  const b = remote[field10] ?? null;
28295
28354
  if (field10 === "embedding_model" && a === null) return true;
@@ -38283,6 +38342,7 @@ function useChatStorage(options) {
38283
38342
  [vaultCtx, vaultEmbeddingCache, memoryStore]
38284
38343
  );
38285
38344
  const chunkVectorCacheRef = (0, import_react7.useRef)(createChunkVectorCache());
38345
+ const recallWarmsRef = (0, import_react7.useRef)(/* @__PURE__ */ new Set());
38286
38346
  const clientToolEmbeddingsCacheRef = (0, import_react7.useRef)(/* @__PURE__ */ new Map());
38287
38347
  (0, import_react7.useEffect)(() => {
38288
38348
  if (!getToken || memoryStore !== void 0) return;
@@ -38296,6 +38356,8 @@ function useChatStorage(options) {
38296
38356
  (0, import_react7.useEffect)(() => {
38297
38357
  return onClearAllEncryptionState(() => {
38298
38358
  vaultEmbeddingCache.clear();
38359
+ for (const warm of recallWarmsRef.current) warm.abort();
38360
+ recallWarmsRef.current.clear();
38299
38361
  chunkVectorCacheRef.current.clear();
38300
38362
  });
38301
38363
  }, [vaultEmbeddingCache]);
@@ -38414,6 +38476,28 @@ function useChatStorage(options) {
38414
38476
  storeMaskMissing
38415
38477
  ]
38416
38478
  );
38479
+ const warmRecallCache = (0, import_react7.useCallback)(
38480
+ async (signal) => {
38481
+ if (!backend.warmChunkCache) return 0;
38482
+ const warm = new AbortController();
38483
+ const stop2 = () => warm.abort(signal?.reason);
38484
+ if (signal?.aborted) stop2();
38485
+ signal?.addEventListener("abort", stop2, { once: true });
38486
+ recallWarmsRef.current.add(warm);
38487
+ try {
38488
+ return await backend.warmChunkCache({
38489
+ chunkCache: chunkVectorCacheRef.current,
38490
+ embeddingModel,
38491
+ limit: DEFAULT_CHUNK_CACHE_SIZE,
38492
+ signal: warm.signal
38493
+ });
38494
+ } finally {
38495
+ recallWarmsRef.current.delete(warm);
38496
+ signal?.removeEventListener("abort", stop2);
38497
+ }
38498
+ },
38499
+ [backend, embeddingModel]
38500
+ );
38417
38501
  const {
38418
38502
  isLoading,
38419
38503
  sendMessage: baseSendMessage,
@@ -39596,6 +39680,7 @@ function useChatStorage(options) {
39596
39680
  createMemoryVaultSearchTool: createMemoryVaultSearchTool2,
39597
39681
  createRecallTool: createRecallTool2,
39598
39682
  recall: recallFn,
39683
+ warmRecallCache,
39599
39684
  searchVaultMemories: searchVaultMemoriesFn,
39600
39685
  vaultEmbeddingCache,
39601
39686
  getVaultMemories,
@@ -4899,6 +4899,13 @@ interface ChatBackend extends ChatStorageAdapter {
4899
4899
  uniqueId: string;
4900
4900
  }): Promise<StoredMessage>;
4901
4901
  searchChunks(queryVector: number[], options?: ChatChunkSearchOptions): Promise<ChunkSearchResult[]>;
4902
+ /** Fills a chunk vector cache ahead of the first search, for backends that score on the device. */
4903
+ warmChunkCache?(options: {
4904
+ chunkCache: ChunkVectorCache;
4905
+ embeddingModel?: string;
4906
+ limit?: number;
4907
+ signal?: AbortSignal;
4908
+ }): Promise<number>;
4902
4909
  getSummary(conversationId: string): Promise<StoredConversationSummary | null>;
4903
4910
  upsertSummary(conversationId: string, summary: string, summarizedUpTo: string, tokenCount: number): Promise<StoredConversationSummary>;
4904
4911
  deleteSummary(conversationId: string): Promise<void>;
@@ -13709,6 +13716,7 @@ declare class WatermelonChatStorageAdapter implements ChatBackend {
13709
13716
  uniqueId: string;
13710
13717
  }): Promise<StoredMessage>;
13711
13718
  searchChunks(queryVector: number[], options?: ChatChunkSearchOptions): Promise<ChunkSearchResult[]>;
13719
+ warmChunkCache(options: Parameters<NonNullable<ChatBackend["warmChunkCache"]>>[0]): Promise<number>;
13712
13720
  getSummary(conversationId: string): Promise<StoredConversationSummary | null>;
13713
13721
  upsertSummary(conversationId: string, summary: string, summarizedUpTo: string, tokenCount: number): Promise<StoredConversationSummary>;
13714
13722
  deleteSummary(conversationId: string): Promise<void>;
@@ -15057,6 +15065,11 @@ interface UseChatStorageResult extends BaseUseChatStorageResult {
15057
15065
  * @param options - Optional recall configuration (types, budget, limit, scopes, …)
15058
15066
  */
15059
15067
  recall: (query: string, options?: RecallOptions) => Promise<RecallResult>;
15068
+ /**
15069
+ * Loads the most recent past messages' vectors, up to the recall cache's size, so the first recall does not read them all.
15070
+ * @returns How many it added. It resolves rather than rejects when aborted or signed out, with how many it added before stopping.
15071
+ */
15072
+ warmRecallCache: (signal?: AbortSignal) => Promise<number>;
15060
15073
  /**
15061
15074
  * Search vault memories programmatically using semantic similarity.
15062
15075
  * Returns structured results sorted by descending similarity.
@@ -4899,6 +4899,13 @@ interface ChatBackend extends ChatStorageAdapter {
4899
4899
  uniqueId: string;
4900
4900
  }): Promise<StoredMessage>;
4901
4901
  searchChunks(queryVector: number[], options?: ChatChunkSearchOptions): Promise<ChunkSearchResult[]>;
4902
+ /** Fills a chunk vector cache ahead of the first search, for backends that score on the device. */
4903
+ warmChunkCache?(options: {
4904
+ chunkCache: ChunkVectorCache;
4905
+ embeddingModel?: string;
4906
+ limit?: number;
4907
+ signal?: AbortSignal;
4908
+ }): Promise<number>;
4902
4909
  getSummary(conversationId: string): Promise<StoredConversationSummary | null>;
4903
4910
  upsertSummary(conversationId: string, summary: string, summarizedUpTo: string, tokenCount: number): Promise<StoredConversationSummary>;
4904
4911
  deleteSummary(conversationId: string): Promise<void>;
@@ -13709,6 +13716,7 @@ declare class WatermelonChatStorageAdapter implements ChatBackend {
13709
13716
  uniqueId: string;
13710
13717
  }): Promise<StoredMessage>;
13711
13718
  searchChunks(queryVector: number[], options?: ChatChunkSearchOptions): Promise<ChunkSearchResult[]>;
13719
+ warmChunkCache(options: Parameters<NonNullable<ChatBackend["warmChunkCache"]>>[0]): Promise<number>;
13712
13720
  getSummary(conversationId: string): Promise<StoredConversationSummary | null>;
13713
13721
  upsertSummary(conversationId: string, summary: string, summarizedUpTo: string, tokenCount: number): Promise<StoredConversationSummary>;
13714
13722
  deleteSummary(conversationId: string): Promise<void>;
@@ -15057,6 +15065,11 @@ interface UseChatStorageResult extends BaseUseChatStorageResult {
15057
15065
  * @param options - Optional recall configuration (types, budget, limit, scopes, …)
15058
15066
  */
15059
15067
  recall: (query: string, options?: RecallOptions) => Promise<RecallResult>;
15068
+ /**
15069
+ * Loads the most recent past messages' vectors, up to the recall cache's size, so the first recall does not read them all.
15070
+ * @returns How many it added. It resolves rather than rejects when aborted or signed out, with how many it added before stopping.
15071
+ */
15072
+ warmRecallCache: (signal?: AbortSignal) => Promise<number>;
15060
15073
  /**
15061
15074
  * Search vault memories programmatically using semantic similarity.
15062
15075
  * Returns structured results sorted by descending similarity.
@@ -3773,6 +3773,63 @@ async function chunkRowSource(ctx, filter) {
3773
3773
  return fullScanChunkRowSource(ctx, filter);
3774
3774
  }
3775
3775
  }
3776
+ async function readChunkVectorRow(ctx, row, onUnreadable) {
3777
+ const version = Number(row.updated_at) || 0;
3778
+ const chunks3 = await readRawJsonField(row, "chunks", ctx.walletAddress);
3779
+ if (chunks3 && chunks3.length > 0) {
3780
+ return {
3781
+ entry: {
3782
+ version,
3783
+ chunks: chunks3.map((chunk) => decodeChunkVector(chunk.vector, onUnreadable))
3784
+ },
3785
+ chunks: chunks3
3786
+ };
3787
+ }
3788
+ const messageVector = await readRawJsonField(row, "vector", ctx.walletAddress);
3789
+ if (!messageVector || messageVector.length === 0) return null;
3790
+ return { entry: { version, chunks: [], fallback: Float32Array.from(messageVector) }, chunks: [] };
3791
+ }
3792
+ async function warmChunkVectorCacheOp(ctx, options) {
3793
+ const { chunkCache, embeddingModel, limit, signal } = options;
3794
+ const source = await chunkRowSource(ctx, {});
3795
+ const yieldIfDue = createTimeSliceYielder(CHUNK_SCAN_YIELD_BUDGET_MS);
3796
+ let keys = source.keys.filter(
3797
+ (key3) => !embeddingModel || key3.embeddingModel === null || key3.embeddingModel === embeddingModel
3798
+ );
3799
+ if (limit !== void 0 && keys.length > limit) {
3800
+ keys = keys.sort((a, b) => b.version - a.version).slice(0, limit);
3801
+ }
3802
+ const missIds = [];
3803
+ for (const key3 of keys) {
3804
+ await yieldIfDue();
3805
+ if (chunkCache.get(key3.id)?.version !== key3.version) missIds.push(key3.id);
3806
+ }
3807
+ let warmed = 0;
3808
+ let unreadableVectors = 0;
3809
+ for await (const row of loadVectorRowsInBatches(source, missIds)) {
3810
+ await yieldIfDue();
3811
+ const read = await readChunkVectorRow(ctx, row, () => {
3812
+ unreadableVectors++;
3813
+ });
3814
+ if (signal?.aborted) break;
3815
+ if (!read) continue;
3816
+ chunkCache.set(row.id, read.entry);
3817
+ warmed++;
3818
+ }
3819
+ warnUnreadableChunkVectors("warmChunkVectorCacheOp", unreadableVectors);
3820
+ return warmed;
3821
+ }
3822
+ async function* loadVectorRowsInBatches(source, ids) {
3823
+ for (let offset = 0; offset < ids.length; offset += CHUNK_VECTOR_BATCH_SIZE) {
3824
+ yield* await source.loadVectorRows(ids.slice(offset, offset + CHUNK_VECTOR_BATCH_SIZE));
3825
+ }
3826
+ }
3827
+ function warnUnreadableChunkVectors(operation, count2) {
3828
+ if (count2 === 0) return;
3829
+ getLogger().warn(
3830
+ `${operation}: could not read ${count2} chunk vectors and left them out of ranking \u2014 the stored value is corrupt, so those chunks stay unsearchable until the message is re-embedded via chunkAndEmbedAllMessages`
3831
+ );
3832
+ }
3776
3833
  async function searchChunksOp(ctx, queryVector, options) {
3777
3834
  const {
3778
3835
  limit = 10,
@@ -3841,45 +3898,39 @@ async function searchChunksOp(ctx, queryVector, options) {
3841
3898
  for (const row of rows) {
3842
3899
  await yieldIfDue();
3843
3900
  const id2 = row.id;
3844
- const version = Number(row.updated_at) || 0;
3845
3901
  const order = orderById.get(id2) ?? Number.MAX_SAFE_INTEGER;
3846
- const chunks3 = await readRawJsonField(row, "chunks", ctx.walletAddress);
3847
- if (chunks3 && chunks3.length > 0) {
3848
- const vectors = chunks3.map(
3849
- (chunk) => decodeChunkVector(chunk.vector, () => {
3850
- unreadableVectors++;
3851
- })
3852
- );
3853
- chunkCache?.set(id2, { version, chunks: vectors });
3854
- for (let ci = 0; ci < chunks3.length; ci++) {
3855
- const vec = vectors[ci];
3856
- if (vec.length === 0) continue;
3857
- const similarity = cosineSimilarity(queryVector, vec);
3858
- if (similarity >= minSimilarity) {
3859
- candidates.push({
3860
- id: id2,
3861
- version,
3862
- order,
3863
- similarity,
3864
- chunkTextSource: { kind: "chunk", chunk: chunks3[ci], siblings: chunks3 }
3865
- });
3866
- }
3902
+ const read = await readChunkVectorRow(ctx, row, () => {
3903
+ unreadableVectors++;
3904
+ });
3905
+ if (!read) continue;
3906
+ const { entry, chunks: chunks3 } = read;
3907
+ const { version } = entry;
3908
+ chunkCache?.set(id2, entry);
3909
+ if (entry.fallback) {
3910
+ const similarity = cosineSimilarity(queryVector, entry.fallback);
3911
+ if (similarity >= minSimilarity) {
3912
+ candidates.push({
3913
+ id: id2,
3914
+ version,
3915
+ order,
3916
+ similarity,
3917
+ chunkTextSource: { kind: "message" }
3918
+ });
3867
3919
  }
3868
- } else {
3869
- const messageVector = await readRawJsonField(row, "vector", ctx.walletAddress);
3870
- if (messageVector && messageVector.length > 0) {
3871
- const fallback = Float32Array.from(messageVector);
3872
- chunkCache?.set(id2, { version, chunks: [], fallback });
3873
- const similarity = cosineSimilarity(queryVector, fallback);
3874
- if (similarity >= minSimilarity) {
3875
- candidates.push({
3876
- id: id2,
3877
- version,
3878
- order,
3879
- similarity,
3880
- chunkTextSource: { kind: "message" }
3881
- });
3882
- }
3920
+ continue;
3921
+ }
3922
+ for (let ci = 0; ci < chunks3.length; ci++) {
3923
+ const vec = entry.chunks[ci];
3924
+ if (vec.length === 0) continue;
3925
+ const similarity = cosineSimilarity(queryVector, vec);
3926
+ if (similarity >= minSimilarity) {
3927
+ candidates.push({
3928
+ id: id2,
3929
+ version,
3930
+ order,
3931
+ similarity,
3932
+ chunkTextSource: { kind: "chunk", chunk: chunks3[ci], siblings: chunks3 }
3933
+ });
3883
3934
  }
3884
3935
  }
3885
3936
  }
@@ -3890,11 +3941,7 @@ async function searchChunksOp(ctx, queryVector, options) {
3890
3941
  `searchChunksOp: skipped ${staleSkipped} messages whose embedding model differs from the current model (${embeddingModel}) \u2014 re-embed via chunkAndEmbedAllMessages`
3891
3942
  );
3892
3943
  }
3893
- if (unreadableVectors > 0) {
3894
- getLogger().warn(
3895
- `searchChunksOp: could not read ${unreadableVectors} chunk vectors and left them out of ranking \u2014 the stored value is corrupt, so those chunks stay unsearchable until the message is re-embedded via chunkAndEmbedAllMessages`
3896
- );
3897
- }
3944
+ warnUnreadableChunkVectors("searchChunksOp", unreadableVectors);
3898
3945
  const topK = candidates.sort((a, b) => b.similarity - a.similarity || a.order - b.order).slice(0, limit);
3899
3946
  const topIds = Array.from(new Set(topK.map((c) => c.id)));
3900
3947
  const fullRows = await source.loadFullRows(topIds);
@@ -11298,7 +11345,7 @@ async function createRemoteMemoryPersistence(options) {
11298
11345
  ...item,
11299
11346
  memory: {
11300
11347
  ...item.memory,
11301
- content: await decrypt(item.memory.content),
11348
+ content: item.memory.is_deleted && item.memory.content === "" ? "" : await decrypt(item.memory.content),
11302
11349
  ...item.memory.kind_value !== void 0 && {
11303
11350
  kind_value: await decrypt(item.memory.kind_value)
11304
11351
  }
@@ -11392,7 +11439,8 @@ async function createRemoteMemoryPersistence(options) {
11392
11439
  }
11393
11440
  if (existing && existing.version !== expectedVersion) throw conflict();
11394
11441
  const decoded = existing ? await decode(existing).catch(() => void 0) : void 0;
11395
- const content = (snapshot?.content ?? decoded?.memory.content) === memory.content ? snapshot?.encryptedContent ?? existing.memory.content : await encryptField2(memory.content);
11442
+ const reusableContent = snapshot?.encryptedContent ?? existing?.memory.content;
11443
+ const content = reusableContent && (snapshot?.content ?? decoded?.memory.content) === memory.content ? reusableContent : await encryptField2(memory.content);
11396
11444
  const kindValue = memory.kind_value === void 0 ? void 0 : (snapshot?.kindValue ?? decoded?.memory.kind_value) === memory.kind_value ? snapshot?.encryptedKindValue ?? existing.memory.kind_value : await encryptField2(memory.kind_value);
11397
11445
  return {
11398
11446
  body: {
@@ -16522,6 +16570,9 @@ var WatermelonChatStorageAdapter = class {
16522
16570
  searchChunks(queryVector, options) {
16523
16571
  return searchChunksOp(this.ctx, queryVector, options);
16524
16572
  }
16573
+ warmChunkCache(options) {
16574
+ return warmChunkVectorCacheOp(this.ctx, options);
16575
+ }
16525
16576
  getSummary(conversationId) {
16526
16577
  return getConversationSummaryOp(
16527
16578
  createSummaryContext(this.ctx.database, this.ctx),
@@ -26216,6 +26267,13 @@ var METADATA_FIELDS = [
26216
26267
  "published_at",
26217
26268
  "geohash"
26218
26269
  ];
26270
+ var TOMBSTONE_FIELDS = [
26271
+ "scope",
26272
+ "is_deleted",
26273
+ "embedding_model",
26274
+ "topics_updated_at",
26275
+ "last_observed_at"
26276
+ ];
26219
26277
  function text15(value) {
26220
26278
  return typeof value === "string" && value !== "" ? value : void 0;
26221
26279
  }
@@ -26363,7 +26421,8 @@ async function embedMissing(prepared, options, model, signal) {
26363
26421
  }
26364
26422
  function sameMetadata2(local, remote, model) {
26365
26423
  const mine = metadata2(local, model);
26366
- return METADATA_FIELDS.every((field10) => {
26424
+ const fields = bool(local.is_deleted) ? TOMBSTONE_FIELDS : METADATA_FIELDS;
26425
+ return fields.every((field10) => {
26367
26426
  const a = mine[field10] ?? null;
26368
26427
  const b = remote[field10] ?? null;
26369
26428
  if (field10 === "embedding_model" && a === null) return true;
@@ -36344,6 +36403,7 @@ function useChatStorage(options) {
36344
36403
  [vaultCtx, vaultEmbeddingCache, memoryStore]
36345
36404
  );
36346
36405
  const chunkVectorCacheRef = useRef4(createChunkVectorCache());
36406
+ const recallWarmsRef = useRef4(/* @__PURE__ */ new Set());
36347
36407
  const clientToolEmbeddingsCacheRef = useRef4(/* @__PURE__ */ new Map());
36348
36408
  useEffect5(() => {
36349
36409
  if (!getToken || memoryStore !== void 0) return;
@@ -36357,6 +36417,8 @@ function useChatStorage(options) {
36357
36417
  useEffect5(() => {
36358
36418
  return onClearAllEncryptionState(() => {
36359
36419
  vaultEmbeddingCache.clear();
36420
+ for (const warm of recallWarmsRef.current) warm.abort();
36421
+ recallWarmsRef.current.clear();
36360
36422
  chunkVectorCacheRef.current.clear();
36361
36423
  });
36362
36424
  }, [vaultEmbeddingCache]);
@@ -36475,6 +36537,28 @@ function useChatStorage(options) {
36475
36537
  storeMaskMissing
36476
36538
  ]
36477
36539
  );
36540
+ const warmRecallCache = useCallback6(
36541
+ async (signal) => {
36542
+ if (!backend.warmChunkCache) return 0;
36543
+ const warm = new AbortController();
36544
+ const stop2 = () => warm.abort(signal?.reason);
36545
+ if (signal?.aborted) stop2();
36546
+ signal?.addEventListener("abort", stop2, { once: true });
36547
+ recallWarmsRef.current.add(warm);
36548
+ try {
36549
+ return await backend.warmChunkCache({
36550
+ chunkCache: chunkVectorCacheRef.current,
36551
+ embeddingModel,
36552
+ limit: DEFAULT_CHUNK_CACHE_SIZE,
36553
+ signal: warm.signal
36554
+ });
36555
+ } finally {
36556
+ recallWarmsRef.current.delete(warm);
36557
+ signal?.removeEventListener("abort", stop2);
36558
+ }
36559
+ },
36560
+ [backend, embeddingModel]
36561
+ );
36478
36562
  const {
36479
36563
  isLoading,
36480
36564
  sendMessage: baseSendMessage,
@@ -37657,6 +37741,7 @@ function useChatStorage(options) {
37657
37741
  createMemoryVaultSearchTool: createMemoryVaultSearchTool2,
37658
37742
  createRecallTool: createRecallTool2,
37659
37743
  recall: recallFn,
37744
+ warmRecallCache,
37660
37745
  searchVaultMemories: searchVaultMemoriesFn,
37661
37746
  vaultEmbeddingCache,
37662
37747
  getVaultMemories,
@@ -19345,6 +19345,28 @@ async function chunkRowSource(ctx, filter) {
19345
19345
  return fullScanChunkRowSource(ctx, filter);
19346
19346
  }
19347
19347
  }
19348
+ async function readChunkVectorRow(ctx, row, onUnreadable) {
19349
+ const version = Number(row.updated_at) || 0;
19350
+ const chunks = await readRawJsonField(row, "chunks", ctx.walletAddress);
19351
+ if (chunks && chunks.length > 0) {
19352
+ return {
19353
+ entry: {
19354
+ version,
19355
+ chunks: chunks.map((chunk) => decodeChunkVector(chunk.vector, onUnreadable))
19356
+ },
19357
+ chunks
19358
+ };
19359
+ }
19360
+ const messageVector = await readRawJsonField(row, "vector", ctx.walletAddress);
19361
+ if (!messageVector || messageVector.length === 0) return null;
19362
+ return { entry: { version, chunks: [], fallback: Float32Array.from(messageVector) }, chunks: [] };
19363
+ }
19364
+ function warnUnreadableChunkVectors(operation, count) {
19365
+ if (count === 0) return;
19366
+ getLogger().warn(
19367
+ `${operation}: could not read ${count} chunk vectors and left them out of ranking \u2014 the stored value is corrupt, so those chunks stay unsearchable until the message is re-embedded via chunkAndEmbedAllMessages`
19368
+ );
19369
+ }
19348
19370
  async function searchChunksOp(ctx, queryVector, options) {
19349
19371
  const {
19350
19372
  limit = 10,
@@ -19413,45 +19435,39 @@ async function searchChunksOp(ctx, queryVector, options) {
19413
19435
  for (const row of rows) {
19414
19436
  await yieldIfDue();
19415
19437
  const id = row.id;
19416
- const version = Number(row.updated_at) || 0;
19417
19438
  const order = orderById.get(id) ?? Number.MAX_SAFE_INTEGER;
19418
- const chunks = await readRawJsonField(row, "chunks", ctx.walletAddress);
19419
- if (chunks && chunks.length > 0) {
19420
- const vectors = chunks.map(
19421
- (chunk) => decodeChunkVector(chunk.vector, () => {
19422
- unreadableVectors++;
19423
- })
19424
- );
19425
- chunkCache?.set(id, { version, chunks: vectors });
19426
- for (let ci = 0; ci < chunks.length; ci++) {
19427
- const vec = vectors[ci];
19428
- if (vec.length === 0) continue;
19429
- const similarity = cosineSimilarity(queryVector, vec);
19430
- if (similarity >= minSimilarity) {
19431
- candidates.push({
19432
- id,
19433
- version,
19434
- order,
19435
- similarity,
19436
- chunkTextSource: { kind: "chunk", chunk: chunks[ci], siblings: chunks }
19437
- });
19438
- }
19439
+ const read = await readChunkVectorRow(ctx, row, () => {
19440
+ unreadableVectors++;
19441
+ });
19442
+ if (!read) continue;
19443
+ const { entry, chunks } = read;
19444
+ const { version } = entry;
19445
+ chunkCache?.set(id, entry);
19446
+ if (entry.fallback) {
19447
+ const similarity = cosineSimilarity(queryVector, entry.fallback);
19448
+ if (similarity >= minSimilarity) {
19449
+ candidates.push({
19450
+ id,
19451
+ version,
19452
+ order,
19453
+ similarity,
19454
+ chunkTextSource: { kind: "message" }
19455
+ });
19439
19456
  }
19440
- } else {
19441
- const messageVector = await readRawJsonField(row, "vector", ctx.walletAddress);
19442
- if (messageVector && messageVector.length > 0) {
19443
- const fallback = Float32Array.from(messageVector);
19444
- chunkCache?.set(id, { version, chunks: [], fallback });
19445
- const similarity = cosineSimilarity(queryVector, fallback);
19446
- if (similarity >= minSimilarity) {
19447
- candidates.push({
19448
- id,
19449
- version,
19450
- order,
19451
- similarity,
19452
- chunkTextSource: { kind: "message" }
19453
- });
19454
- }
19457
+ continue;
19458
+ }
19459
+ for (let ci = 0; ci < chunks.length; ci++) {
19460
+ const vec = entry.chunks[ci];
19461
+ if (vec.length === 0) continue;
19462
+ const similarity = cosineSimilarity(queryVector, vec);
19463
+ if (similarity >= minSimilarity) {
19464
+ candidates.push({
19465
+ id,
19466
+ version,
19467
+ order,
19468
+ similarity,
19469
+ chunkTextSource: { kind: "chunk", chunk: chunks[ci], siblings: chunks }
19470
+ });
19455
19471
  }
19456
19472
  }
19457
19473
  }
@@ -19462,11 +19478,7 @@ async function searchChunksOp(ctx, queryVector, options) {
19462
19478
  `searchChunksOp: skipped ${staleSkipped} messages whose embedding model differs from the current model (${embeddingModel}) \u2014 re-embed via chunkAndEmbedAllMessages`
19463
19479
  );
19464
19480
  }
19465
- if (unreadableVectors > 0) {
19466
- getLogger().warn(
19467
- `searchChunksOp: could not read ${unreadableVectors} chunk vectors and left them out of ranking \u2014 the stored value is corrupt, so those chunks stay unsearchable until the message is re-embedded via chunkAndEmbedAllMessages`
19468
- );
19469
- }
19481
+ warnUnreadableChunkVectors("searchChunksOp", unreadableVectors);
19470
19482
  const topK = candidates.sort((a, b) => b.similarity - a.similarity || a.order - b.order).slice(0, limit);
19471
19483
  const topIds = Array.from(new Set(topK.map((c) => c.id)));
19472
19484
  const fullRows = await source.loadFullRows(topIds);
@@ -4100,6 +4100,13 @@ interface ChatBackend extends ChatStorageAdapter {
4100
4100
  uniqueId: string;
4101
4101
  }): Promise<StoredMessage>;
4102
4102
  searchChunks(queryVector: number[], options?: ChatChunkSearchOptions): Promise<ChunkSearchResult[]>;
4103
+ /** Fills a chunk vector cache ahead of the first search, for backends that score on the device. */
4104
+ warmChunkCache?(options: {
4105
+ chunkCache: ChunkVectorCache;
4106
+ embeddingModel?: string;
4107
+ limit?: number;
4108
+ signal?: AbortSignal;
4109
+ }): Promise<number>;
4103
4110
  getSummary(conversationId: string): Promise<StoredConversationSummary | null>;
4104
4111
  upsertSummary(conversationId: string, summary: string, summarizedUpTo: string, tokenCount: number): Promise<StoredConversationSummary>;
4105
4112
  deleteSummary(conversationId: string): Promise<void>;
@@ -4100,6 +4100,13 @@ interface ChatBackend extends ChatStorageAdapter {
4100
4100
  uniqueId: string;
4101
4101
  }): Promise<StoredMessage>;
4102
4102
  searchChunks(queryVector: number[], options?: ChatChunkSearchOptions): Promise<ChunkSearchResult[]>;
4103
+ /** Fills a chunk vector cache ahead of the first search, for backends that score on the device. */
4104
+ warmChunkCache?(options: {
4105
+ chunkCache: ChunkVectorCache;
4106
+ embeddingModel?: string;
4107
+ limit?: number;
4108
+ signal?: AbortSignal;
4109
+ }): Promise<number>;
4103
4110
  getSummary(conversationId: string): Promise<StoredConversationSummary | null>;
4104
4111
  upsertSummary(conversationId: string, summary: string, summarizedUpTo: string, tokenCount: number): Promise<StoredConversationSummary>;
4105
4112
  deleteSummary(conversationId: string): Promise<void>;
@@ -17637,6 +17637,28 @@ async function chunkRowSource(ctx, filter) {
17637
17637
  return fullScanChunkRowSource(ctx, filter);
17638
17638
  }
17639
17639
  }
17640
+ async function readChunkVectorRow(ctx, row, onUnreadable) {
17641
+ const version = Number(row.updated_at) || 0;
17642
+ const chunks = await readRawJsonField(row, "chunks", ctx.walletAddress);
17643
+ if (chunks && chunks.length > 0) {
17644
+ return {
17645
+ entry: {
17646
+ version,
17647
+ chunks: chunks.map((chunk) => decodeChunkVector(chunk.vector, onUnreadable))
17648
+ },
17649
+ chunks
17650
+ };
17651
+ }
17652
+ const messageVector = await readRawJsonField(row, "vector", ctx.walletAddress);
17653
+ if (!messageVector || messageVector.length === 0) return null;
17654
+ return { entry: { version, chunks: [], fallback: Float32Array.from(messageVector) }, chunks: [] };
17655
+ }
17656
+ function warnUnreadableChunkVectors(operation, count) {
17657
+ if (count === 0) return;
17658
+ getLogger().warn(
17659
+ `${operation}: could not read ${count} chunk vectors and left them out of ranking \u2014 the stored value is corrupt, so those chunks stay unsearchable until the message is re-embedded via chunkAndEmbedAllMessages`
17660
+ );
17661
+ }
17640
17662
  async function searchChunksOp(ctx, queryVector, options) {
17641
17663
  const {
17642
17664
  limit = 10,
@@ -17705,45 +17727,39 @@ async function searchChunksOp(ctx, queryVector, options) {
17705
17727
  for (const row of rows) {
17706
17728
  await yieldIfDue();
17707
17729
  const id = row.id;
17708
- const version = Number(row.updated_at) || 0;
17709
17730
  const order = orderById.get(id) ?? Number.MAX_SAFE_INTEGER;
17710
- const chunks = await readRawJsonField(row, "chunks", ctx.walletAddress);
17711
- if (chunks && chunks.length > 0) {
17712
- const vectors = chunks.map(
17713
- (chunk) => decodeChunkVector(chunk.vector, () => {
17714
- unreadableVectors++;
17715
- })
17716
- );
17717
- chunkCache?.set(id, { version, chunks: vectors });
17718
- for (let ci = 0; ci < chunks.length; ci++) {
17719
- const vec = vectors[ci];
17720
- if (vec.length === 0) continue;
17721
- const similarity = cosineSimilarity(queryVector, vec);
17722
- if (similarity >= minSimilarity) {
17723
- candidates.push({
17724
- id,
17725
- version,
17726
- order,
17727
- similarity,
17728
- chunkTextSource: { kind: "chunk", chunk: chunks[ci], siblings: chunks }
17729
- });
17730
- }
17731
+ const read = await readChunkVectorRow(ctx, row, () => {
17732
+ unreadableVectors++;
17733
+ });
17734
+ if (!read) continue;
17735
+ const { entry, chunks } = read;
17736
+ const { version } = entry;
17737
+ chunkCache?.set(id, entry);
17738
+ if (entry.fallback) {
17739
+ const similarity = cosineSimilarity(queryVector, entry.fallback);
17740
+ if (similarity >= minSimilarity) {
17741
+ candidates.push({
17742
+ id,
17743
+ version,
17744
+ order,
17745
+ similarity,
17746
+ chunkTextSource: { kind: "message" }
17747
+ });
17731
17748
  }
17732
- } else {
17733
- const messageVector = await readRawJsonField(row, "vector", ctx.walletAddress);
17734
- if (messageVector && messageVector.length > 0) {
17735
- const fallback = Float32Array.from(messageVector);
17736
- chunkCache?.set(id, { version, chunks: [], fallback });
17737
- const similarity = cosineSimilarity(queryVector, fallback);
17738
- if (similarity >= minSimilarity) {
17739
- candidates.push({
17740
- id,
17741
- version,
17742
- order,
17743
- similarity,
17744
- chunkTextSource: { kind: "message" }
17745
- });
17746
- }
17749
+ continue;
17750
+ }
17751
+ for (let ci = 0; ci < chunks.length; ci++) {
17752
+ const vec = entry.chunks[ci];
17753
+ if (vec.length === 0) continue;
17754
+ const similarity = cosineSimilarity(queryVector, vec);
17755
+ if (similarity >= minSimilarity) {
17756
+ candidates.push({
17757
+ id,
17758
+ version,
17759
+ order,
17760
+ similarity,
17761
+ chunkTextSource: { kind: "chunk", chunk: chunks[ci], siblings: chunks }
17762
+ });
17747
17763
  }
17748
17764
  }
17749
17765
  }
@@ -17754,11 +17770,7 @@ async function searchChunksOp(ctx, queryVector, options) {
17754
17770
  `searchChunksOp: skipped ${staleSkipped} messages whose embedding model differs from the current model (${embeddingModel}) \u2014 re-embed via chunkAndEmbedAllMessages`
17755
17771
  );
17756
17772
  }
17757
- if (unreadableVectors > 0) {
17758
- getLogger().warn(
17759
- `searchChunksOp: could not read ${unreadableVectors} chunk vectors and left them out of ranking \u2014 the stored value is corrupt, so those chunks stay unsearchable until the message is re-embedded via chunkAndEmbedAllMessages`
17760
- );
17761
- }
17773
+ warnUnreadableChunkVectors("searchChunksOp", unreadableVectors);
17762
17774
  const topK = candidates.sort((a, b) => b.similarity - a.similarity || a.order - b.order).slice(0, limit);
17763
17775
  const topIds = Array.from(new Set(topK.map((c) => c.id)));
17764
17776
  const fullRows = await source.loadFullRows(topIds);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anuma/sdk",
3
- "version": "10.5.2",
3
+ "version": "10.5.4",
4
4
  "description": "",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",