@anuma/sdk 7.0.0 → 7.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1495,6 +1495,7 @@ __export(index_exports, {
1495
1495
  embedAllMessages: () => embedAllMessages,
1496
1496
  embedMessage: () => embedMessage,
1497
1497
  encodeChunkVector: () => encodeChunkVector,
1498
+ encryptToolCallEventsOp: () => encryptToolCallEventsOp,
1498
1499
  ensureDefaultFoldersOp: () => ensureDefaultFoldersOp,
1499
1500
  exchangeNotionCode: () => exchangeNotionCode,
1500
1501
  extractAndLinkEntitiesForMemoriesOp: () => extractAndLinkEntitiesForMemoriesOp,
@@ -2883,6 +2884,35 @@ function decodeChunkVector(vector, onMalformed) {
2883
2884
  }
2884
2885
  }
2885
2886
 
2887
+ // src/lib/scheduling.ts
2888
+ function nowMs() {
2889
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
2890
+ }
2891
+ function yieldToEventLoop() {
2892
+ const scheduler = globalThis.scheduler;
2893
+ if (typeof scheduler?.yield === "function") return scheduler.yield();
2894
+ if (typeof MessageChannel === "function") {
2895
+ return new Promise((resolve) => {
2896
+ const channel = new MessageChannel();
2897
+ channel.port1.onmessage = () => {
2898
+ channel.port1.close();
2899
+ channel.port2.close();
2900
+ resolve();
2901
+ };
2902
+ channel.port2.postMessage(null);
2903
+ });
2904
+ }
2905
+ return new Promise((resolve) => setTimeout(resolve, 0));
2906
+ }
2907
+ function createTimeSliceYielder(budgetMs) {
2908
+ let sliceStart = nowMs();
2909
+ return async () => {
2910
+ if (nowMs() - sliceStart < budgetMs) return;
2911
+ await yieldToEventLoop();
2912
+ sliceStart = nowMs();
2913
+ };
2914
+ }
2915
+
2886
2916
  // src/lib/db/encryption-utils.ts
2887
2917
  init_logger();
2888
2918
  var ENCRYPTION_PREFIX = "enc:v3:";
@@ -2892,6 +2922,11 @@ function isEncrypted(value) {
2892
2922
  const payload = value.slice(prefix.length);
2893
2923
  return payload.length >= 56 && /^[0-9a-f]+$/i.test(payload);
2894
2924
  }
2925
+ function hasEncryptedToolPayload(event) {
2926
+ return [event.arguments, event.output].some(
2927
+ (value) => typeof value === "string" && isEncrypted(value)
2928
+ );
2929
+ }
2895
2930
  function detectEncryptionVersion(value) {
2896
2931
  if (value.startsWith("enc:v3:")) {
2897
2932
  return { version: "v3", encryptedData: value.slice("enc:v3:".length) };
@@ -3029,6 +3064,12 @@ async function encryptMessageFields(message, address, signMessage, embeddedWalle
3029
3064
  const encryptedChunks = msg.chunks ? await encryptJsonField(msg.chunks, address, signMessage, embeddedWalletSigner, true) : void 0;
3030
3065
  const encryptedSources = msg.sources ? await encryptJsonField(msg.sources, address, signMessage, embeddedWalletSigner, true) : void 0;
3031
3066
  const encryptedThoughtProcess = msg.thoughtProcess ? await encryptJsonField(msg.thoughtProcess, address, signMessage, embeddedWalletSigner, true) : void 0;
3067
+ const encryptedToolCallEvents = Array.isArray(msg.toolCallEvents) ? await encryptToolCallEvents(
3068
+ msg.toolCallEvents,
3069
+ address,
3070
+ signMessage,
3071
+ embeddedWalletSigner
3072
+ ) : void 0;
3032
3073
  return {
3033
3074
  ...message,
3034
3075
  ...encryptedContent !== void 0 && { content: encryptedContent },
@@ -3036,13 +3077,31 @@ async function encryptMessageFields(message, address, signMessage, embeddedWalle
3036
3077
  ...encryptedVector !== void 0 && { vector: encryptedVector },
3037
3078
  ...encryptedChunks !== void 0 && { chunks: encryptedChunks },
3038
3079
  ...encryptedSources !== void 0 && { sources: encryptedSources },
3039
- ...encryptedThoughtProcess !== void 0 && { thoughtProcess: encryptedThoughtProcess }
3080
+ ...encryptedThoughtProcess !== void 0 && { thoughtProcess: encryptedThoughtProcess },
3081
+ ...encryptedToolCallEvents !== void 0 && { toolCallEvents: encryptedToolCallEvents }
3040
3082
  };
3041
3083
  } catch (error) {
3042
3084
  getLogger().warn("Failed to encrypt message fields:", error);
3043
3085
  throw error;
3044
3086
  }
3045
3087
  }
3088
+ function mapToolCallPayloads(events, transform) {
3089
+ return Promise.all(
3090
+ events.map(async (event) => {
3091
+ if (!event || typeof event !== "object") return event;
3092
+ const mapped = { ...event };
3093
+ if (typeof event.arguments === "string") mapped.arguments = await transform(event.arguments);
3094
+ if (typeof event.output === "string") mapped.output = await transform(event.output);
3095
+ return mapped;
3096
+ })
3097
+ );
3098
+ }
3099
+ function encryptToolCallEvents(events, address, signMessage, embeddedWalletSigner) {
3100
+ return mapToolCallPayloads(
3101
+ events,
3102
+ (value) => encryptField(value, address, signMessage, embeddedWalletSigner, true)
3103
+ );
3104
+ }
3046
3105
  async function decryptMaybeJsonFieldDetailed(value, address) {
3047
3106
  if (value === void 0) return { value: void 0, status: "plaintext" };
3048
3107
  if (value === null) return { value, status: "plaintext" };
@@ -3128,6 +3187,10 @@ async function assembleDecryptedMessage(message, address) {
3128
3187
  message.thoughtProcess,
3129
3188
  address
3130
3189
  );
3190
+ const toolCallEventsResult = await decryptMaybeJsonFieldDetailed(
3191
+ message.toolCallEvents,
3192
+ address
3193
+ );
3131
3194
  let decryptionStatus;
3132
3195
  let refreshProbe;
3133
3196
  const consider = (status, probe) => {
@@ -3146,6 +3209,14 @@ async function assembleDecryptedMessage(message, address) {
3146
3209
  if (thoughtProcessResult.probe) {
3147
3210
  consider(thoughtProcessResult.status, thoughtProcessResult.probe);
3148
3211
  }
3212
+ if (toolCallEventsResult.probe) {
3213
+ consider(toolCallEventsResult.status, toolCallEventsResult.probe);
3214
+ }
3215
+ const toolCallEvents = Array.isArray(toolCallEventsResult.value) ? await mapToolCallPayloads(toolCallEventsResult.value, async (value) => {
3216
+ const detailed = await decryptFieldDetailed(value, address);
3217
+ consider(detailed.status, isEncrypted(value) ? value : void 0);
3218
+ return detailed.value;
3219
+ }) : void 0;
3149
3220
  const statusForConsumer = decryptionStatus === "ok" || decryptionStatus === "plaintext" || !decryptionStatus ? void 0 : decryptionStatus;
3150
3221
  const result = {
3151
3222
  ...message,
@@ -3154,7 +3225,8 @@ async function assembleDecryptedMessage(message, address) {
3154
3225
  vector: vectorResult.value,
3155
3226
  chunks: chunksResult.value,
3156
3227
  sources: sourcesResult.value,
3157
- thoughtProcess: thoughtProcessResult.value
3228
+ thoughtProcess: thoughtProcessResult.value,
3229
+ toolCallEvents
3158
3230
  };
3159
3231
  if (statusForConsumer) {
3160
3232
  result.decryptionStatus = statusForConsumer;
@@ -3950,7 +4022,7 @@ async function updateMessageChunksOp(ctx, uniqueId, chunks3, embeddingModel) {
3950
4022
  });
3951
4023
  return messageToStored(message, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner);
3952
4024
  }
3953
- var STRIP_CHUNK_TEXT_PAGE_SIZE = 100;
4025
+ var RAW_REPAIR_PAGE_SIZE = 100;
3954
4026
  function chunksWithoutText(raw) {
3955
4027
  if (typeof raw !== "string") return null;
3956
4028
  let parsed;
@@ -3973,8 +4045,8 @@ function chunksWithoutText(raw) {
3973
4045
  async function stripLegacyChunkTextOp(ctx) {
3974
4046
  const candidateIds = await ctx.messagesCollection.query(import_watermelondb2.Q.where("chunks", import_watermelondb2.Q.like(`%${import_watermelondb2.Q.sanitizeLikeString('"text":')}%`))).fetchIds();
3975
4047
  let stripped = 0;
3976
- for (let start = 0; start < candidateIds.length; start += STRIP_CHUNK_TEXT_PAGE_SIZE) {
3977
- const pageIds = candidateIds.slice(start, start + STRIP_CHUNK_TEXT_PAGE_SIZE);
4048
+ for (let start = 0; start < candidateIds.length; start += RAW_REPAIR_PAGE_SIZE) {
4049
+ const pageIds = candidateIds.slice(start, start + RAW_REPAIR_PAGE_SIZE);
3978
4050
  const rows = await ctx.messagesCollection.query(import_watermelondb2.Q.where("id", import_watermelondb2.Q.oneOf(pageIds))).unsafeFetchRaw();
3979
4051
  const changedIds = rows.filter((row) => chunksWithoutText(row.chunks) !== null).map((row) => String(row.id));
3980
4052
  if (changedIds.length === 0) continue;
@@ -3998,6 +4070,88 @@ async function stripLegacyChunkTextOp(ctx) {
3998
4070
  }
3999
4071
  return stripped;
4000
4072
  }
4073
+ async function encryptedToolCallEventsColumn(raw, address, signMessage, embeddedWalletSigner) {
4074
+ if (typeof raw !== "string") return null;
4075
+ let parsed;
4076
+ try {
4077
+ parsed = JSON.parse(raw);
4078
+ } catch {
4079
+ return null;
4080
+ }
4081
+ if (!Array.isArray(parsed)) return null;
4082
+ const column = JSON.stringify(
4083
+ await encryptToolCallEvents(
4084
+ parsed,
4085
+ address,
4086
+ signMessage,
4087
+ embeddedWalletSigner
4088
+ )
4089
+ );
4090
+ return column === JSON.stringify(parsed) ? null : column;
4091
+ }
4092
+ async function encryptToolCallEventsOp(ctx) {
4093
+ const { walletAddress: address, signMessage, embeddedWalletSigner } = ctx;
4094
+ if (!address || !signMessage) return 0;
4095
+ if (!await requestEncryptionKey(address, signMessage, embeddedWalletSigner)) return 0;
4096
+ if (!hasEncryptionKey(address)) return 0;
4097
+ if (!await savedKeyOpensExistingData(ctx, address, signMessage, embeddedWalletSigner)) {
4098
+ return 0;
4099
+ }
4100
+ const candidateIds = await ctx.messagesCollection.query(
4101
+ import_watermelondb2.Q.where("tool_call_events", import_watermelondb2.Q.notEq(null)),
4102
+ import_watermelondb2.Q.where("tool_call_events", import_watermelondb2.Q.notIn(["", "[]"]))
4103
+ ).fetchIds();
4104
+ let encrypted = 0;
4105
+ for (let start = 0; start < candidateIds.length; start += RAW_REPAIR_PAGE_SIZE) {
4106
+ const pageIds = candidateIds.slice(start, start + RAW_REPAIR_PAGE_SIZE);
4107
+ const rows = await ctx.messagesCollection.query(import_watermelondb2.Q.where("id", import_watermelondb2.Q.oneOf(pageIds))).unsafeFetchRaw();
4108
+ const repairs = /* @__PURE__ */ new Map();
4109
+ for (const row of rows) {
4110
+ const column = await encryptedToolCallEventsColumn(
4111
+ row.tool_call_events,
4112
+ address,
4113
+ signMessage,
4114
+ embeddedWalletSigner
4115
+ );
4116
+ if (column !== null) repairs.set(String(row.id), { raw: row.tool_call_events, column });
4117
+ }
4118
+ if (repairs.size === 0) continue;
4119
+ await ctx.database.write(async () => {
4120
+ const messages = (await ctx.messagesCollection.query(import_watermelondb2.Q.where("id", import_watermelondb2.Q.oneOf([...repairs.keys()]))).fetch()).filter(
4121
+ (message) => message._raw.tool_call_events === repairs.get(message.id)?.raw
4122
+ );
4123
+ if (messages.length === 0) return;
4124
+ const conversationIds = [...new Set(messages.map((message) => message.conversationId))];
4125
+ const conversations = await ctx.conversationsCollection.query(import_watermelondb2.Q.where("conversation_id", import_watermelondb2.Q.oneOf(conversationIds))).fetch();
4126
+ await ctx.database.batch(
4127
+ ...messages.map(
4128
+ (message) => message.prepareUpdate((msg) => {
4129
+ msg._setRaw("tool_call_events", repairs.get(message.id).column);
4130
+ })
4131
+ ),
4132
+ ...conversations.map((conversation) => conversation.prepareUpdate(() => {
4133
+ }))
4134
+ );
4135
+ encrypted += messages.length;
4136
+ });
4137
+ }
4138
+ return encrypted;
4139
+ }
4140
+ async function savedKeyOpensExistingData(ctx, address, signMessage, embeddedWalletSigner) {
4141
+ const v3Probe = await findEncryptedContent(ctx, "enc:v3:%");
4142
+ if (v3Probe) {
4143
+ const { status } = await decryptFieldDetailed(v3Probe, address);
4144
+ if (status !== "auth_mismatch" && status !== "key_missing") return true;
4145
+ return refreshEncryptionKeyIfMatches(address, v3Probe, signMessage, embeddedWalletSigner);
4146
+ }
4147
+ const v2Probe = await findEncryptedContent(ctx, "enc:v2:%");
4148
+ if (!v2Probe) return true;
4149
+ return refreshEncryptionKeyIfMatches(address, v2Probe, signMessage, embeddedWalletSigner);
4150
+ }
4151
+ async function findEncryptedContent(ctx, pattern) {
4152
+ const [row] = await ctx.messagesCollection.query(import_watermelondb2.Q.where("content", import_watermelondb2.Q.like(pattern)), import_watermelondb2.Q.take(1)).unsafeFetchRaw();
4153
+ return row ? String(row.content) : null;
4154
+ }
4001
4155
  async function updateMessageErrorOp(ctx, uniqueId, error) {
4002
4156
  let message;
4003
4157
  try {
@@ -4095,6 +4249,95 @@ async function readRawJsonField(raw, column, walletAddress) {
4095
4249
  return void 0;
4096
4250
  }
4097
4251
  }
4252
+ var CHUNK_VECTOR_BATCH_SIZE = 50;
4253
+ var CHUNK_SCAN_YIELD_BUDGET_MS = 12;
4254
+ var hasEmbeddingSql = `(coalesce("chunks", '') != '' or coalesce("vector", '') != '')`;
4255
+ function toChunkCandidateKey(raw) {
4256
+ return {
4257
+ id: raw.id,
4258
+ conversationId: String(raw.conversation_id),
4259
+ version: Number(raw.updated_at) || 0,
4260
+ embeddingModel: raw.embedding_model ?? null
4261
+ };
4262
+ }
4263
+ async function fetchRowsByIds(ctx, ids) {
4264
+ if (ids.length === 0) return [];
4265
+ return await ctx.messagesCollection.query(import_watermelondb2.Q.where("id", import_watermelondb2.Q.oneOf(ids))).unsafeFetchRaw();
4266
+ }
4267
+ async function projectedChunkRowSource(ctx, filter) {
4268
+ const clauses = [
4269
+ `"history"."_status" is not 'deleted'`,
4270
+ hasEmbeddingSql,
4271
+ `exists (select 1 from "conversations" where "conversations"."conversation_id" = "history"."conversation_id" and "conversations"."is_deleted" = 0 and "conversations"."_status" is not 'deleted')`
4272
+ ];
4273
+ const args = [];
4274
+ if (filter.conversationId !== void 0) {
4275
+ clauses.push(`"history"."conversation_id" = ?`);
4276
+ args.push(filter.conversationId);
4277
+ }
4278
+ if (filter.excludeConversationId !== void 0) {
4279
+ clauses.push(`"history"."conversation_id" != ?`);
4280
+ args.push(filter.excludeConversationId);
4281
+ }
4282
+ const keyRows = await ctx.messagesCollection.query(
4283
+ import_watermelondb2.Q.unsafeSqlQuery(
4284
+ `select "id", "conversation_id", "updated_at", "embedding_model" from "history" where ${clauses.join(" and ")}`,
4285
+ args
4286
+ )
4287
+ ).unsafeFetchRaw();
4288
+ return {
4289
+ keys: keyRows.map(toChunkCandidateKey),
4290
+ loadVectorRows: async (ids) => {
4291
+ if (ids.length === 0) return [];
4292
+ return await ctx.messagesCollection.query(
4293
+ import_watermelondb2.Q.unsafeSqlQuery(
4294
+ `select "id", "updated_at", "chunks", "vector" from "history" where "_status" is not 'deleted' and "id" in (${ids.map(() => "?").join(",")})`,
4295
+ ids
4296
+ )
4297
+ ).unsafeFetchRaw();
4298
+ },
4299
+ loadFullRows: (ids) => fetchRowsByIds(ctx, ids)
4300
+ };
4301
+ }
4302
+ async function fullScanChunkRowSource(ctx, filter) {
4303
+ const activeConversations = await ctx.conversationsCollection.query(import_watermelondb2.Q.where("is_deleted", false)).fetch();
4304
+ const activeConversationIds = new Set(
4305
+ activeConversations.map((c) => String(c._getRaw("conversation_id")))
4306
+ );
4307
+ const queryConditions = filter.conversationId !== void 0 ? [import_watermelondb2.Q.where("conversation_id", filter.conversationId)] : [];
4308
+ const rows = await ctx.messagesCollection.query(...queryConditions).unsafeFetchRaw();
4309
+ const rowsById = /* @__PURE__ */ new Map();
4310
+ for (const row of rows) {
4311
+ const conversationId = String(row.conversation_id);
4312
+ if (!activeConversationIds.has(conversationId)) continue;
4313
+ if (filter.excludeConversationId !== void 0 && conversationId === filter.excludeConversationId) {
4314
+ continue;
4315
+ }
4316
+ if (!row.chunks && !row.vector) continue;
4317
+ rowsById.set(row.id, row);
4318
+ }
4319
+ const pick = (ids) => Promise.resolve(
4320
+ ids.flatMap((id) => {
4321
+ const row = rowsById.get(id);
4322
+ return row ? [row] : [];
4323
+ })
4324
+ );
4325
+ return {
4326
+ keys: Array.from(rowsById.values(), toChunkCandidateKey),
4327
+ loadVectorRows: pick,
4328
+ loadFullRows: pick
4329
+ };
4330
+ }
4331
+ async function chunkRowSource(ctx, filter) {
4332
+ try {
4333
+ return await projectedChunkRowSource(ctx, filter);
4334
+ } catch (err) {
4335
+ getLogger().debug(
4336
+ "searchChunksOp: projected SQL unavailable, using full-load fallback: " + (err instanceof Error ? err.message : String(err))
4337
+ );
4338
+ return fullScanChunkRowSource(ctx, filter);
4339
+ }
4340
+ }
4098
4341
  async function searchChunksOp(ctx, queryVector, options) {
4099
4342
  const {
4100
4343
  limit = 10,
@@ -4102,83 +4345,111 @@ async function searchChunksOp(ctx, queryVector, options) {
4102
4345
  conversationId,
4103
4346
  excludeConversationId,
4104
4347
  embeddingModel,
4105
- chunkCache
4348
+ chunkCache,
4349
+ onStats
4106
4350
  } = options || {};
4107
- const activeConversations = await ctx.conversationsCollection.query(import_watermelondb2.Q.where("is_deleted", false)).fetch();
4108
- const activeConversationIds = new Set(
4109
- activeConversations.map((c) => String(c._getRaw("conversation_id")))
4110
- );
4111
- const queryConditions = conversationId ? [import_watermelondb2.Q.where("conversation_id", conversationId)] : [];
4112
- const messages = await ctx.messagesCollection.query(...queryConditions).unsafeFetchRaw();
4351
+ const source = await chunkRowSource(ctx, { conversationId, excludeConversationId });
4352
+ const yieldIfDue = createTimeSliceYielder(CHUNK_SCAN_YIELD_BUDGET_MS);
4113
4353
  const candidates = [];
4114
4354
  let staleSkipped = 0;
4115
4355
  let unreadableVectors = 0;
4116
- for (const message of messages) {
4117
- const msgConvId = String(message.conversation_id);
4118
- if (!activeConversationIds.has(msgConvId)) continue;
4119
- if (excludeConversationId !== void 0 && msgConvId === excludeConversationId) continue;
4120
- if (embeddingModel) {
4121
- const storedModel = message.embedding_model;
4122
- if (storedModel !== null && storedModel !== void 0 && storedModel !== embeddingModel) {
4123
- staleSkipped++;
4124
- continue;
4125
- }
4356
+ let cacheHits = 0;
4357
+ const missIds = [];
4358
+ const orderById = /* @__PURE__ */ new Map();
4359
+ for (const [order, key] of source.keys.entries()) {
4360
+ await yieldIfDue();
4361
+ orderById.set(key.id, order);
4362
+ if (embeddingModel && key.embeddingModel !== null && key.embeddingModel !== embeddingModel) {
4363
+ staleSkipped++;
4364
+ continue;
4126
4365
  }
4127
- const version = Number(message.updated_at) || 0;
4128
- const cached = chunkCache?.get(message.id);
4129
- if (cached && cached.version === version) {
4130
- if (cached.chunks.length > 0) {
4131
- for (let ci = 0; ci < cached.chunks.length; ci++) {
4132
- const vec = cached.chunks[ci];
4133
- if (vec.length === 0) continue;
4134
- const similarity = cosineSimilarity(queryVector, vec);
4135
- if (similarity >= minSimilarity) {
4136
- candidates.push({
4137
- message,
4138
- similarity,
4139
- chunkTextSource: { kind: "chunk-cached", chunkIndex: ci, version }
4140
- });
4141
- }
4142
- }
4143
- } else if (cached.fallback) {
4144
- const similarity = cosineSimilarity(queryVector, cached.fallback);
4145
- if (similarity >= minSimilarity) {
4146
- candidates.push({ message, similarity, chunkTextSource: { kind: "message" } });
4147
- }
4148
- }
4366
+ const cached = chunkCache?.get(key.id);
4367
+ if (!cached || cached.version !== key.version) {
4368
+ missIds.push(key.id);
4149
4369
  continue;
4150
4370
  }
4151
- const chunks3 = await readRawJsonField(message, "chunks", ctx.walletAddress);
4152
- if (chunks3 && chunks3.length > 0) {
4153
- const vectors = chunks3.map(
4154
- (chunk) => decodeChunkVector(chunk.vector, () => {
4155
- unreadableVectors++;
4156
- })
4157
- );
4158
- chunkCache?.set(message.id, { version, chunks: vectors });
4159
- for (let ci = 0; ci < chunks3.length; ci++) {
4160
- const vec = vectors[ci];
4371
+ cacheHits++;
4372
+ if (cached.chunks.length > 0) {
4373
+ for (let ci = 0; ci < cached.chunks.length; ci++) {
4374
+ const vec = cached.chunks[ci];
4161
4375
  if (vec.length === 0) continue;
4162
4376
  const similarity = cosineSimilarity(queryVector, vec);
4163
4377
  if (similarity >= minSimilarity) {
4164
4378
  candidates.push({
4165
- message,
4379
+ id: key.id,
4380
+ version: key.version,
4381
+ order,
4166
4382
  similarity,
4167
- chunkTextSource: { kind: "chunk", chunk: chunks3[ci], siblings: chunks3 }
4383
+ chunkTextSource: { kind: "chunk-cached", chunkIndex: ci, version: key.version }
4168
4384
  });
4169
4385
  }
4170
4386
  }
4171
- } else {
4172
- const messageVector = await readRawJsonField(message, "vector", ctx.walletAddress);
4173
- if (!messageVector || messageVector.length === 0) continue;
4174
- const fallback = Float32Array.from(messageVector);
4175
- chunkCache?.set(message.id, { version, chunks: [], fallback });
4176
- const similarity = cosineSimilarity(queryVector, fallback);
4387
+ } else if (cached.fallback) {
4388
+ const similarity = cosineSimilarity(queryVector, cached.fallback);
4177
4389
  if (similarity >= minSimilarity) {
4178
- candidates.push({ message, similarity, chunkTextSource: { kind: "message" } });
4390
+ candidates.push({
4391
+ id: key.id,
4392
+ version: key.version,
4393
+ order,
4394
+ similarity,
4395
+ chunkTextSource: { kind: "message" }
4396
+ });
4179
4397
  }
4180
4398
  }
4181
4399
  }
4400
+ let rowsFetched = 0;
4401
+ for (let offset = 0; offset < missIds.length; offset += CHUNK_VECTOR_BATCH_SIZE) {
4402
+ const rows = await source.loadVectorRows(
4403
+ missIds.slice(offset, offset + CHUNK_VECTOR_BATCH_SIZE)
4404
+ );
4405
+ rowsFetched += rows.length;
4406
+ for (const row of rows) {
4407
+ await yieldIfDue();
4408
+ const id = row.id;
4409
+ const version = Number(row.updated_at) || 0;
4410
+ const order = orderById.get(id) ?? Number.MAX_SAFE_INTEGER;
4411
+ const chunks3 = await readRawJsonField(row, "chunks", ctx.walletAddress);
4412
+ if (chunks3 && chunks3.length > 0) {
4413
+ const vectors = chunks3.map(
4414
+ (chunk) => decodeChunkVector(chunk.vector, () => {
4415
+ unreadableVectors++;
4416
+ })
4417
+ );
4418
+ chunkCache?.set(id, { version, chunks: vectors });
4419
+ for (let ci = 0; ci < chunks3.length; ci++) {
4420
+ const vec = vectors[ci];
4421
+ if (vec.length === 0) continue;
4422
+ const similarity = cosineSimilarity(queryVector, vec);
4423
+ if (similarity >= minSimilarity) {
4424
+ candidates.push({
4425
+ id,
4426
+ version,
4427
+ order,
4428
+ similarity,
4429
+ chunkTextSource: { kind: "chunk", chunk: chunks3[ci], siblings: chunks3 }
4430
+ });
4431
+ }
4432
+ }
4433
+ } else {
4434
+ const messageVector = await readRawJsonField(row, "vector", ctx.walletAddress);
4435
+ if (messageVector && messageVector.length > 0) {
4436
+ const fallback = Float32Array.from(messageVector);
4437
+ chunkCache?.set(id, { version, chunks: [], fallback });
4438
+ const similarity = cosineSimilarity(queryVector, fallback);
4439
+ if (similarity >= minSimilarity) {
4440
+ candidates.push({
4441
+ id,
4442
+ version,
4443
+ order,
4444
+ similarity,
4445
+ chunkTextSource: { kind: "message" }
4446
+ });
4447
+ }
4448
+ }
4449
+ }
4450
+ }
4451
+ }
4452
+ onStats?.({ rowsScanned: source.keys.length, rowsFetched, cacheHits });
4182
4453
  if (staleSkipped > 0) {
4183
4454
  getLogger().warn(
4184
4455
  `searchChunksOp: skipped ${staleSkipped} messages whose embedding model differs from the current model (${embeddingModel}) \u2014 re-embed via chunkAndEmbedAllMessages`
@@ -4189,12 +4460,17 @@ async function searchChunksOp(ctx, queryVector, options) {
4189
4460
  `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`
4190
4461
  );
4191
4462
  }
4192
- const topK = candidates.sort((a, b) => b.similarity - a.similarity).slice(0, limit);
4193
- const uniqueMessages = /* @__PURE__ */ new Map();
4194
- for (const c of topK) uniqueMessages.set(c.message.id, c.message);
4463
+ const topK = candidates.sort((a, b) => b.similarity - a.similarity || a.order - b.order).slice(0, limit);
4464
+ const topIds = Array.from(new Set(topK.map((c) => c.id)));
4465
+ const fullRows = await source.loadFullRows(topIds);
4466
+ const rowById = new Map(fullRows.map((row) => [row.id, row]));
4467
+ const liveTopK = topK.filter((c) => {
4468
+ const row = rowById.get(c.id);
4469
+ return row !== void 0 && (Number(row.updated_at) || 0) === c.version;
4470
+ });
4195
4471
  const storedById = new Map(
4196
4472
  await Promise.all(
4197
- Array.from(uniqueMessages, async ([id, m]) => {
4473
+ Array.from(rowById, async ([id, m]) => {
4198
4474
  const stored2 = await messageRawToStored(
4199
4475
  m,
4200
4476
  ctx.walletAddress,
@@ -4207,24 +4483,24 @@ async function searchChunksOp(ctx, queryVector, options) {
4207
4483
  );
4208
4484
  const chunkStateByMsgId = /* @__PURE__ */ new Map();
4209
4485
  const needsChunkText = /* @__PURE__ */ new Set();
4210
- for (const c of topK) {
4211
- if (c.chunkTextSource.kind === "chunk-cached") needsChunkText.add(c.message.id);
4486
+ for (const c of liveTopK) {
4487
+ if (c.chunkTextSource.kind === "chunk-cached") needsChunkText.add(c.id);
4212
4488
  }
4213
4489
  await Promise.all(
4214
4490
  Array.from(needsChunkText, async (id) => {
4215
- const m = uniqueMessages.get(id);
4491
+ const m = rowById.get(id);
4216
4492
  const parsed = await readRawJsonField(m, "chunks", ctx.walletAddress);
4217
4493
  const version = Number(m.updated_at) || 0;
4218
4494
  if (parsed) chunkStateByMsgId.set(id, { version, chunks: parsed });
4219
4495
  })
4220
4496
  );
4221
- return topK.map(({ message, similarity, chunkTextSource }) => {
4222
- const stored2 = { ...storedById.get(message.id) };
4497
+ return liveTopK.map(({ id, similarity, chunkTextSource }) => {
4498
+ const stored2 = { ...storedById.get(id) };
4223
4499
  let chunkText2;
4224
4500
  if (chunkTextSource.kind === "chunk") {
4225
4501
  chunkText2 = resolveChunkText(stored2.content, chunkTextSource.chunk, chunkTextSource.siblings);
4226
4502
  } else if (chunkTextSource.kind === "chunk-cached") {
4227
- const state = chunkStateByMsgId.get(message.id);
4503
+ const state = chunkStateByMsgId.get(id);
4228
4504
  const chunk = state && state.version === chunkTextSource.version ? state.chunks[chunkTextSource.chunkIndex] : void 0;
4229
4505
  chunkText2 = resolveChunkText(stored2.content, chunk, state?.chunks);
4230
4506
  } else {
@@ -5309,13 +5585,35 @@ async function generateEmbedding(text14, options) {
5309
5585
  if (total === void 0) return embedOne(text14, options, void 0);
5310
5586
  return withDeadline((signal) => embedOne(text14, options, signal), total, "embedding");
5311
5587
  }
5588
+ var inFlightByCache = /* @__PURE__ */ new WeakMap();
5312
5589
  async function embedOne(text14, options, outer) {
5313
- const { baseUrl = BASE_URL, model, cache } = options;
5314
- const timeoutMs = options.timeoutMs ?? DEFAULT_EMBEDDING_REQUEST_TIMEOUT_MS;
5315
- if (cache) {
5316
- const cached = cache.get(text14);
5317
- if (cached) return Array.from(cached);
5590
+ const { cache } = options;
5591
+ if (!cache) return Array.from(await requestEmbedding(text14, options, outer));
5592
+ const cached = cache.get(text14);
5593
+ if (cached) return Array.from(cached);
5594
+ const inFlight = inFlightByCache.get(cache) ?? /* @__PURE__ */ new Map();
5595
+ inFlightByCache.set(cache, inFlight);
5596
+ const pending = inFlight.get(text14);
5597
+ if (pending) {
5598
+ try {
5599
+ return Array.from(await pending);
5600
+ } catch (err) {
5601
+ if (outer?.aborted) throw err;
5602
+ }
5603
+ }
5604
+ const request = requestEmbedding(text14, options, outer);
5605
+ inFlight.set(text14, request);
5606
+ try {
5607
+ const embedding2 = await request;
5608
+ cache.set(text14, embedding2);
5609
+ return Array.from(embedding2);
5610
+ } finally {
5611
+ if (inFlight.get(text14) === request) inFlight.delete(text14);
5318
5612
  }
5613
+ }
5614
+ async function requestEmbedding(text14, options, outer) {
5615
+ const { baseUrl = BASE_URL, model } = options;
5616
+ const timeoutMs = options.timeoutMs ?? DEFAULT_EMBEDDING_REQUEST_TIMEOUT_MS;
5319
5617
  const headers = {
5320
5618
  ...await resolveAuthHeaders(options, outer),
5321
5619
  ...taskTypeHeader(options.taskType)
@@ -5350,11 +5648,7 @@ async function embedOne(text14, options, outer) {
5350
5648
  totalTokens: response.data.usage.total_tokens ?? 0
5351
5649
  });
5352
5650
  }
5353
- const f32Embedding = Float32Array.from(embedding2);
5354
- if (cache) {
5355
- cache.set(text14, f32Embedding);
5356
- }
5357
- return Array.from(f32Embedding);
5651
+ return Float32Array.from(embedding2);
5358
5652
  }
5359
5653
  var DEFAULT_EMBEDDING_BATCH_SIZE = 100;
5360
5654
  var DEFAULT_EMBEDDING_BATCH_CONCURRENCY = 3;
@@ -5752,7 +6046,8 @@ function extractReservationReceipts(toolCallEvents) {
5752
6046
  for (const event of toolCallEvents ?? []) {
5753
6047
  const action = ACTIONS.find((a) => event.name?.endsWith(a.tool));
5754
6048
  const output = event.output ?? "";
5755
- if (!action || !output || NOT_AN_ATTEMPT.test(output)) continue;
6049
+ if (!action || !output || hasEncryptedToolPayload(event) || NOT_AN_ATTEMPT.test(output))
6050
+ continue;
5756
6051
  const kept = latest.get(action.kind)?.status;
5757
6052
  if (kept === "made") continue;
5758
6053
  const receipt = toReceipt(action, event.arguments ?? "", output);
@@ -10849,8 +11144,12 @@ var MaskScopedEmbeddingCache = class extends Map {
10849
11144
  return this;
10850
11145
  }
10851
11146
  };
11147
+ var scopedViews = /* @__PURE__ */ new WeakMap();
10852
11148
  function maskScopedEmbeddingCache(cache, masked) {
10853
- return new MaskScopedEmbeddingCache(cache, masked);
11149
+ const views = scopedViews.get(cache) ?? {};
11150
+ scopedViews.set(cache, views);
11151
+ const slot = masked ? "masked" : "raw";
11152
+ return views[slot] ?? (views[slot] = new MaskScopedEmbeddingCache(cache, masked));
10854
11153
  }
10855
11154
 
10856
11155
  // src/lib/db/conversationMemory/models.ts
@@ -15211,9 +15510,9 @@ function recencyMultiplier(updatedAt, opts = {}) {
15211
15510
  const updatedMs = updatedAt.getTime();
15212
15511
  if (!Number.isFinite(updatedMs)) return noDate;
15213
15512
  const now = opts.now ?? /* @__PURE__ */ new Date();
15214
- const nowMs3 = now.getTime();
15215
- if (!Number.isFinite(nowMs3)) return noDate;
15216
- const ageDays2 = Math.max(0, (nowMs3 - updatedMs) / DAY_MS2);
15513
+ const nowMs2 = now.getTime();
15514
+ if (!Number.isFinite(nowMs2)) return noDate;
15515
+ const ageDays2 = Math.max(0, (nowMs2 - updatedMs) / DAY_MS2);
15217
15516
  const perYearDecay = opts.perYearDecay ?? DEFAULT_PER_YEAR_DECAY;
15218
15517
  const floor = opts.floor ?? DEFAULT_FLOOR;
15219
15518
  const decay = perYearDecay * (ageDays2 / 365);
@@ -15725,9 +16024,6 @@ function createVaultEmbeddingCache(maxSize = DEFAULT_VAULT_CACHE_SIZE) {
15725
16024
 
15726
16025
  // src/lib/memoryVault/searchTool.ts
15727
16026
  var DEFAULT_RERANK_TOP_N = 5;
15728
- function nowMs() {
15729
- return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
15730
- }
15731
16027
  var warnedProgrammaticDecomposeIgnored = false;
15732
16028
  function normalizeEventTimeKind(kind) {
15733
16029
  return kind === "point" || kind === "range" || kind === "ongoing" ? kind : null;
@@ -18839,9 +19135,6 @@ function scoreEventTimeOverlap(memoryStart, memoryEnd, memoryKind, window2) {
18839
19135
  var DEFAULT_LIMIT = 8;
18840
19136
  var DEFAULT_BUDGET = "low";
18841
19137
  var DEFAULT_FACT_MIN_SCORE = 0.1;
18842
- function nowMs2() {
18843
- return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
18844
- }
18845
19138
  var DEFAULT_CHUNK_MIN_SCORE = 0.5;
18846
19139
  var DEFAULT_QUERY_EMBED_TOTAL_TIMEOUT_MS = 8e3;
18847
19140
  function flagsForBudget(budget) {
@@ -18879,12 +19172,19 @@ async function recall(query, ctx, options = {}) {
18879
19172
  const subQueries = normalizedFacets.length >= 2 ? normalizedFacets : void 0;
18880
19173
  const embeddingOptions = {
18881
19174
  ...ctx.embeddingOptions,
18882
- taskType: "memory_recall_embed"
19175
+ taskType: "memory_recall_embed",
19176
+ ...options.embeddingCache && !ctx.embeddingOptions.cache && {
19177
+ cache: maskScopedEmbeddingCache(
19178
+ options.embeddingCache,
19179
+ Boolean(ctx.embeddingOptions.maskInput)
19180
+ )
19181
+ }
18883
19182
  };
18884
- const t0 = nowMs2();
19183
+ const t0 = nowMs();
18885
19184
  let prepMs = 0;
18886
19185
  let factLaneMs = 0;
18887
19186
  let chunkLaneMs = 0;
19187
+ let chunkStats;
18888
19188
  let fuseMs = 0;
18889
19189
  const factResults = [];
18890
19190
  const chunkResults = [];
@@ -18939,6 +19239,11 @@ async function recall(query, ctx, options = {}) {
18939
19239
  ...decryptLastRan !== void 0 && { decryptLast: decryptLastRan },
18940
19240
  ...vaultRowsDecrypted !== void 0 && { vaultRowsDecrypted },
18941
19241
  ...vaultRowsEmbedded !== void 0 && { vaultRowsEmbedded },
19242
+ ...chunkStats && {
19243
+ chunkRowsScanned: chunkStats.rowsScanned,
19244
+ chunkRowsFetched: chunkStats.rowsFetched,
19245
+ chunkCacheHits: chunkStats.cacheHits
19246
+ },
18942
19247
  factCount: factResults.length,
18943
19248
  chunkCount: chunkResults.length,
18944
19249
  admittedCount: admitted.length,
@@ -18950,7 +19255,7 @@ async function recall(query, ctx, options = {}) {
18950
19255
  temporalLaneCount,
18951
19256
  emptyReason: emptyReasonFor(admitted.length, laneRan),
18952
19257
  timings: {
18953
- total: nowMs2() - t0,
19258
+ total: nowMs() - t0,
18954
19259
  prep: prepMs,
18955
19260
  factLane: factLaneMs,
18956
19261
  rerank: rerankMs,
@@ -18972,14 +19277,14 @@ async function recall(query, ctx, options = {}) {
18972
19277
  const needsChunkEmbedding = types.includes("chunk") && ctx.storageCtx;
18973
19278
  const wantsTemporal = types.includes("fact") && (ctx.factSource || ctx.vaultCtx);
18974
19279
  const queryEmbedTotalTimeoutMs = options.queryEmbedTotalTimeoutMs ?? DEFAULT_QUERY_EMBED_TOTAL_TIMEOUT_MS;
18975
- const prepStart = nowMs2();
19280
+ const prepStart = nowMs();
18976
19281
  let sharedEmbedMs = 0;
18977
19282
  const [queryEmbedding, entityRanking, temporalRanking] = await Promise.all([
18978
19283
  needsChunkEmbedding ? generateEmbedding(query, {
18979
19284
  ...embeddingOptions,
18980
19285
  totalTimeoutMs: queryEmbedTotalTimeoutMs
18981
19286
  }).finally(() => {
18982
- sharedEmbedMs = nowMs2() - prepStart;
19287
+ sharedEmbedMs = nowMs() - prepStart;
18983
19288
  }).then((vec) => {
18984
19289
  if (vec.length === 0) {
18985
19290
  getLogger().warn(
@@ -19012,11 +19317,11 @@ async function recall(query, ctx, options = {}) {
19012
19317
  () => ctx.factSource ? ctx.factSource.temporalRanking(query, options.now) : buildTemporalLaneRanking(query, ctx.vaultCtx, options.now)
19013
19318
  ) : Promise.resolve([])
19014
19319
  ]);
19015
- prepMs = nowMs2() - prepStart;
19320
+ prepMs = nowMs() - prepStart;
19016
19321
  graphLaneCount = entityRanking.length;
19017
19322
  temporalLaneCount = temporalRanking.length;
19018
19323
  if (types.includes("fact") && (ctx.factSource || ctx.vaultCtx && ctx.vaultCache)) {
19019
- const factStart = nowMs2();
19324
+ const factStart = nowMs();
19020
19325
  const vaultMinScore = options.minScore ?? DEFAULT_FACT_MIN_SCORE;
19021
19326
  factFloor = vaultMinScore;
19022
19327
  const {
@@ -19092,11 +19397,11 @@ async function recall(query, ctx, options = {}) {
19092
19397
  factLaneRankedOnCosine = factRankedOnCosine;
19093
19398
  decryptLastRan = factDecryptLast;
19094
19399
  vaultRowsDecrypted = factRowsDecrypted;
19095
- factLaneMs = nowMs2() - factStart;
19400
+ factLaneMs = nowMs() - factStart;
19096
19401
  }
19097
19402
  if (chunkEmbedFailed && !factLaneRankedOnCosine) embeddingsUnavailable = true;
19098
19403
  if (types.includes("chunk") && ctx.storageCtx && queryEmbedding) {
19099
- const chunkStart = nowMs2();
19404
+ const chunkStart = nowMs();
19100
19405
  const chunkMinScore = options.minScore ?? DEFAULT_CHUNK_MIN_SCORE;
19101
19406
  chunkFloor = chunkMinScore;
19102
19407
  const results = await searchChunksOp(ctx.storageCtx, queryEmbedding, {
@@ -19107,12 +19412,15 @@ async function recall(query, ctx, options = {}) {
19107
19412
  ...options.excludeConversationId && {
19108
19413
  excludeConversationId: options.excludeConversationId
19109
19414
  },
19110
- ...ctx.chunkCache && { chunkCache: ctx.chunkCache }
19415
+ ...ctx.chunkCache && { chunkCache: ctx.chunkCache },
19416
+ onStats: (stats) => {
19417
+ chunkStats = stats;
19418
+ }
19111
19419
  });
19112
19420
  chunkResults.push(...dedupeBy(results, (r) => r.chunkText.trim()));
19113
- chunkLaneMs = nowMs2() - chunkStart;
19421
+ chunkLaneMs = nowMs() - chunkStart;
19114
19422
  }
19115
- const fuseStart = nowMs2();
19423
+ const fuseStart = nowMs();
19116
19424
  if (types.length === 1 || factResults.length === 0 || chunkResults.length === 0) {
19117
19425
  const memories2 = [
19118
19426
  ...factResults.map((r) => toFactMemory(r, options.now)),
@@ -19120,7 +19428,7 @@ async function recall(query, ctx, options = {}) {
19120
19428
  ];
19121
19429
  memories2.sort((a, b) => b.score - a.score);
19122
19430
  const candidateCount = factResults.length + chunkResults.length;
19123
- fuseMs = nowMs2() - fuseStart;
19431
+ fuseMs = nowMs() - fuseStart;
19124
19432
  const admitted = memories2.slice(0, limit);
19125
19433
  hitLimit = memories2.length > limit;
19126
19434
  emitDiagnostics(candidateCount, admitted);
@@ -19184,7 +19492,7 @@ async function recall(query, ctx, options = {}) {
19184
19492
  memories = selection.out;
19185
19493
  }
19186
19494
  hitLimit = selection.cut;
19187
- fuseMs = nowMs2() - fuseStart;
19495
+ fuseMs = nowMs() - fuseStart;
19188
19496
  emitDiagnostics(byId.size, memories);
19189
19497
  return {
19190
19498
  memories,
@@ -26831,11 +27139,12 @@ function storedToLlmapiMessage(stored2) {
26831
27139
  }
26832
27140
  }
26833
27141
  const messages = [];
26834
- if (stored2.role === "assistant" && stored2.toolCallEvents?.length) {
27142
+ const toolCallEvents = Array.isArray(stored2.toolCallEvents) ? stored2.toolCallEvents.filter((event) => !hasEncryptedToolPayload(event)) : [];
27143
+ if (stored2.role === "assistant" && toolCallEvents.length) {
26835
27144
  messages.push({
26836
27145
  role: stored2.role,
26837
27146
  content: void 0,
26838
- tool_calls: stored2.toolCallEvents.map((event) => ({
27147
+ tool_calls: toolCallEvents.map((event) => ({
26839
27148
  id: event.id,
26840
27149
  type: "function",
26841
27150
  function: {
@@ -26844,7 +27153,7 @@ function storedToLlmapiMessage(stored2) {
26844
27153
  }
26845
27154
  }))
26846
27155
  });
26847
- for (const event of stored2.toolCallEvents) {
27156
+ for (const event of toolCallEvents) {
26848
27157
  if (event.id && event.output !== void 0 && event.output !== null) {
26849
27158
  messages.push({
26850
27159
  role: "tool",
@@ -28848,6 +29157,7 @@ function useChatStorage(options) {
28848
29157
  embedAllMessages,
28849
29158
  embedMessage,
28850
29159
  encodeChunkVector,
29160
+ encryptToolCallEventsOp,
28851
29161
  ensureDefaultFoldersOp,
28852
29162
  exchangeNotionCode,
28853
29163
  extractAndLinkEntitiesForMemoriesOp,