@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.
- package/dist/expo/index.cjs +104 -45
- package/dist/expo/index.d.mts +7 -0
- package/dist/expo/index.d.ts +7 -0
- package/dist/expo/index.mjs +104 -45
- package/dist/react/index.cjs +130 -45
- package/dist/react/index.d.mts +13 -0
- package/dist/react/index.d.ts +13 -0
- package/dist/react/index.mjs +130 -45
- package/dist/server/index.cjs +54 -42
- package/dist/server/index.d.mts +7 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.mjs +54 -42
- package/package.json +1 -1
package/dist/expo/index.cjs
CHANGED
|
@@ -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
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
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
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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),
|
package/dist/expo/index.d.mts
CHANGED
|
@@ -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>;
|
package/dist/expo/index.d.ts
CHANGED
|
@@ -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>;
|
package/dist/expo/index.mjs
CHANGED
|
@@ -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
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
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
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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),
|
package/dist/react/index.cjs
CHANGED
|
@@ -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
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
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
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
package/dist/react/index.d.mts
CHANGED
|
@@ -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.
|
package/dist/react/index.d.ts
CHANGED
|
@@ -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.
|
package/dist/react/index.mjs
CHANGED
|
@@ -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
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
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
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
package/dist/server/index.cjs
CHANGED
|
@@ -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
|
|
19419
|
-
|
|
19420
|
-
|
|
19421
|
-
|
|
19422
|
-
|
|
19423
|
-
|
|
19424
|
-
|
|
19425
|
-
|
|
19426
|
-
|
|
19427
|
-
|
|
19428
|
-
|
|
19429
|
-
|
|
19430
|
-
|
|
19431
|
-
|
|
19432
|
-
|
|
19433
|
-
|
|
19434
|
-
|
|
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
|
-
|
|
19441
|
-
|
|
19442
|
-
|
|
19443
|
-
|
|
19444
|
-
|
|
19445
|
-
|
|
19446
|
-
|
|
19447
|
-
|
|
19448
|
-
|
|
19449
|
-
|
|
19450
|
-
|
|
19451
|
-
|
|
19452
|
-
|
|
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
|
-
|
|
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);
|
package/dist/server/index.d.mts
CHANGED
|
@@ -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>;
|
package/dist/server/index.d.ts
CHANGED
|
@@ -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>;
|
package/dist/server/index.mjs
CHANGED
|
@@ -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
|
|
17711
|
-
|
|
17712
|
-
|
|
17713
|
-
|
|
17714
|
-
|
|
17715
|
-
|
|
17716
|
-
|
|
17717
|
-
|
|
17718
|
-
|
|
17719
|
-
|
|
17720
|
-
|
|
17721
|
-
|
|
17722
|
-
|
|
17723
|
-
|
|
17724
|
-
|
|
17725
|
-
|
|
17726
|
-
|
|
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
|
-
|
|
17733
|
-
|
|
17734
|
-
|
|
17735
|
-
|
|
17736
|
-
|
|
17737
|
-
|
|
17738
|
-
|
|
17739
|
-
|
|
17740
|
-
|
|
17741
|
-
|
|
17742
|
-
|
|
17743
|
-
|
|
17744
|
-
|
|
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
|
-
|
|
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);
|