@anuma/sdk 10.5.2 → 10.5.3

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);
@@ -29907,6 +29954,9 @@ var WatermelonChatStorageAdapter = class {
29907
29954
  searchChunks(queryVector, options) {
29908
29955
  return searchChunksOp(this.ctx, queryVector, options);
29909
29956
  }
29957
+ warmChunkCache(options) {
29958
+ return warmChunkVectorCacheOp(this.ctx, options);
29959
+ }
29910
29960
  getSummary(conversationId) {
29911
29961
  return getConversationSummaryOp(
29912
29962
  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);
@@ -28178,6 +28225,9 @@ var WatermelonChatStorageAdapter = class {
28178
28225
  searchChunks(queryVector, options) {
28179
28226
  return searchChunksOp(this.ctx, queryVector, options);
28180
28227
  }
28228
+ warmChunkCache(options) {
28229
+ return warmChunkVectorCacheOp(this.ctx, options);
28230
+ }
28181
28231
  getSummary(conversationId) {
28182
28232
  return getConversationSummaryOp(
28183
28233
  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);
@@ -18392,6 +18439,9 @@ var WatermelonChatStorageAdapter = class {
18392
18439
  searchChunks(queryVector, options) {
18393
18440
  return searchChunksOp(this.ctx, queryVector, options);
18394
18441
  }
18442
+ warmChunkCache(options) {
18443
+ return warmChunkVectorCacheOp(this.ctx, options);
18444
+ }
18395
18445
  getSummary(conversationId) {
18396
18446
  return getConversationSummaryOp(
18397
18447
  createSummaryContext(this.ctx.database, this.ctx),
@@ -38283,6 +38333,7 @@ function useChatStorage(options) {
38283
38333
  [vaultCtx, vaultEmbeddingCache, memoryStore]
38284
38334
  );
38285
38335
  const chunkVectorCacheRef = (0, import_react7.useRef)(createChunkVectorCache());
38336
+ const recallWarmsRef = (0, import_react7.useRef)(/* @__PURE__ */ new Set());
38286
38337
  const clientToolEmbeddingsCacheRef = (0, import_react7.useRef)(/* @__PURE__ */ new Map());
38287
38338
  (0, import_react7.useEffect)(() => {
38288
38339
  if (!getToken || memoryStore !== void 0) return;
@@ -38296,6 +38347,8 @@ function useChatStorage(options) {
38296
38347
  (0, import_react7.useEffect)(() => {
38297
38348
  return onClearAllEncryptionState(() => {
38298
38349
  vaultEmbeddingCache.clear();
38350
+ for (const warm of recallWarmsRef.current) warm.abort();
38351
+ recallWarmsRef.current.clear();
38299
38352
  chunkVectorCacheRef.current.clear();
38300
38353
  });
38301
38354
  }, [vaultEmbeddingCache]);
@@ -38414,6 +38467,28 @@ function useChatStorage(options) {
38414
38467
  storeMaskMissing
38415
38468
  ]
38416
38469
  );
38470
+ const warmRecallCache = (0, import_react7.useCallback)(
38471
+ async (signal) => {
38472
+ if (!backend.warmChunkCache) return 0;
38473
+ const warm = new AbortController();
38474
+ const stop2 = () => warm.abort(signal?.reason);
38475
+ if (signal?.aborted) stop2();
38476
+ signal?.addEventListener("abort", stop2, { once: true });
38477
+ recallWarmsRef.current.add(warm);
38478
+ try {
38479
+ return await backend.warmChunkCache({
38480
+ chunkCache: chunkVectorCacheRef.current,
38481
+ embeddingModel,
38482
+ limit: DEFAULT_CHUNK_CACHE_SIZE,
38483
+ signal: warm.signal
38484
+ });
38485
+ } finally {
38486
+ recallWarmsRef.current.delete(warm);
38487
+ signal?.removeEventListener("abort", stop2);
38488
+ }
38489
+ },
38490
+ [backend, embeddingModel]
38491
+ );
38417
38492
  const {
38418
38493
  isLoading,
38419
38494
  sendMessage: baseSendMessage,
@@ -39596,6 +39671,7 @@ function useChatStorage(options) {
39596
39671
  createMemoryVaultSearchTool: createMemoryVaultSearchTool2,
39597
39672
  createRecallTool: createRecallTool2,
39598
39673
  recall: recallFn,
39674
+ warmRecallCache,
39599
39675
  searchVaultMemories: searchVaultMemoriesFn,
39600
39676
  vaultEmbeddingCache,
39601
39677
  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);
@@ -16522,6 +16569,9 @@ var WatermelonChatStorageAdapter = class {
16522
16569
  searchChunks(queryVector, options) {
16523
16570
  return searchChunksOp(this.ctx, queryVector, options);
16524
16571
  }
16572
+ warmChunkCache(options) {
16573
+ return warmChunkVectorCacheOp(this.ctx, options);
16574
+ }
16525
16575
  getSummary(conversationId) {
16526
16576
  return getConversationSummaryOp(
16527
16577
  createSummaryContext(this.ctx.database, this.ctx),
@@ -36344,6 +36394,7 @@ function useChatStorage(options) {
36344
36394
  [vaultCtx, vaultEmbeddingCache, memoryStore]
36345
36395
  );
36346
36396
  const chunkVectorCacheRef = useRef4(createChunkVectorCache());
36397
+ const recallWarmsRef = useRef4(/* @__PURE__ */ new Set());
36347
36398
  const clientToolEmbeddingsCacheRef = useRef4(/* @__PURE__ */ new Map());
36348
36399
  useEffect5(() => {
36349
36400
  if (!getToken || memoryStore !== void 0) return;
@@ -36357,6 +36408,8 @@ function useChatStorage(options) {
36357
36408
  useEffect5(() => {
36358
36409
  return onClearAllEncryptionState(() => {
36359
36410
  vaultEmbeddingCache.clear();
36411
+ for (const warm of recallWarmsRef.current) warm.abort();
36412
+ recallWarmsRef.current.clear();
36360
36413
  chunkVectorCacheRef.current.clear();
36361
36414
  });
36362
36415
  }, [vaultEmbeddingCache]);
@@ -36475,6 +36528,28 @@ function useChatStorage(options) {
36475
36528
  storeMaskMissing
36476
36529
  ]
36477
36530
  );
36531
+ const warmRecallCache = useCallback6(
36532
+ async (signal) => {
36533
+ if (!backend.warmChunkCache) return 0;
36534
+ const warm = new AbortController();
36535
+ const stop2 = () => warm.abort(signal?.reason);
36536
+ if (signal?.aborted) stop2();
36537
+ signal?.addEventListener("abort", stop2, { once: true });
36538
+ recallWarmsRef.current.add(warm);
36539
+ try {
36540
+ return await backend.warmChunkCache({
36541
+ chunkCache: chunkVectorCacheRef.current,
36542
+ embeddingModel,
36543
+ limit: DEFAULT_CHUNK_CACHE_SIZE,
36544
+ signal: warm.signal
36545
+ });
36546
+ } finally {
36547
+ recallWarmsRef.current.delete(warm);
36548
+ signal?.removeEventListener("abort", stop2);
36549
+ }
36550
+ },
36551
+ [backend, embeddingModel]
36552
+ );
36478
36553
  const {
36479
36554
  isLoading,
36480
36555
  sendMessage: baseSendMessage,
@@ -37657,6 +37732,7 @@ function useChatStorage(options) {
37657
37732
  createMemoryVaultSearchTool: createMemoryVaultSearchTool2,
37658
37733
  createRecallTool: createRecallTool2,
37659
37734
  recall: recallFn,
37735
+ warmRecallCache,
37660
37736
  searchVaultMemories: searchVaultMemoriesFn,
37661
37737
  vaultEmbeddingCache,
37662
37738
  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.3",
4
4
  "description": "",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",