@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.
@@ -1813,6 +1813,35 @@ function decodeChunkVector(vector, onMalformed) {
1813
1813
  }
1814
1814
  }
1815
1815
 
1816
+ // src/lib/scheduling.ts
1817
+ function nowMs() {
1818
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
1819
+ }
1820
+ function yieldToEventLoop() {
1821
+ const scheduler = globalThis.scheduler;
1822
+ if (typeof scheduler?.yield === "function") return scheduler.yield();
1823
+ if (typeof MessageChannel === "function") {
1824
+ return new Promise((resolve) => {
1825
+ const channel = new MessageChannel();
1826
+ channel.port1.onmessage = () => {
1827
+ channel.port1.close();
1828
+ channel.port2.close();
1829
+ resolve();
1830
+ };
1831
+ channel.port2.postMessage(null);
1832
+ });
1833
+ }
1834
+ return new Promise((resolve) => setTimeout(resolve, 0));
1835
+ }
1836
+ function createTimeSliceYielder(budgetMs) {
1837
+ let sliceStart = nowMs();
1838
+ return async () => {
1839
+ if (nowMs() - sliceStart < budgetMs) return;
1840
+ await yieldToEventLoop();
1841
+ sliceStart = nowMs();
1842
+ };
1843
+ }
1844
+
1816
1845
  // src/lib/db/encryption-utils.ts
1817
1846
  var ENCRYPTION_PREFIX = "enc:v3:";
1818
1847
  function isEncrypted(value) {
@@ -1821,6 +1850,11 @@ function isEncrypted(value) {
1821
1850
  const payload = value.slice(prefix.length);
1822
1851
  return payload.length >= 56 && /^[0-9a-f]+$/i.test(payload);
1823
1852
  }
1853
+ function hasEncryptedToolPayload(event) {
1854
+ return [event.arguments, event.output].some(
1855
+ (value) => typeof value === "string" && isEncrypted(value)
1856
+ );
1857
+ }
1824
1858
  function detectEncryptionVersion(value) {
1825
1859
  if (value.startsWith("enc:v3:")) {
1826
1860
  return { version: "v3", encryptedData: value.slice("enc:v3:".length) };
@@ -1956,6 +1990,12 @@ async function encryptMessageFields(message, address, signMessage, embeddedWalle
1956
1990
  const encryptedChunks = msg.chunks ? await encryptJsonField(msg.chunks, address, signMessage, embeddedWalletSigner, true) : void 0;
1957
1991
  const encryptedSources = msg.sources ? await encryptJsonField(msg.sources, address, signMessage, embeddedWalletSigner, true) : void 0;
1958
1992
  const encryptedThoughtProcess = msg.thoughtProcess ? await encryptJsonField(msg.thoughtProcess, address, signMessage, embeddedWalletSigner, true) : void 0;
1993
+ const encryptedToolCallEvents = Array.isArray(msg.toolCallEvents) ? await encryptToolCallEvents(
1994
+ msg.toolCallEvents,
1995
+ address,
1996
+ signMessage,
1997
+ embeddedWalletSigner
1998
+ ) : void 0;
1959
1999
  return {
1960
2000
  ...message,
1961
2001
  ...encryptedContent !== void 0 && { content: encryptedContent },
@@ -1963,13 +2003,31 @@ async function encryptMessageFields(message, address, signMessage, embeddedWalle
1963
2003
  ...encryptedVector !== void 0 && { vector: encryptedVector },
1964
2004
  ...encryptedChunks !== void 0 && { chunks: encryptedChunks },
1965
2005
  ...encryptedSources !== void 0 && { sources: encryptedSources },
1966
- ...encryptedThoughtProcess !== void 0 && { thoughtProcess: encryptedThoughtProcess }
2006
+ ...encryptedThoughtProcess !== void 0 && { thoughtProcess: encryptedThoughtProcess },
2007
+ ...encryptedToolCallEvents !== void 0 && { toolCallEvents: encryptedToolCallEvents }
1967
2008
  };
1968
2009
  } catch (error) {
1969
2010
  getLogger().warn("Failed to encrypt message fields:", error);
1970
2011
  throw error;
1971
2012
  }
1972
2013
  }
2014
+ function mapToolCallPayloads(events, transform) {
2015
+ return Promise.all(
2016
+ events.map(async (event) => {
2017
+ if (!event || typeof event !== "object") return event;
2018
+ const mapped = { ...event };
2019
+ if (typeof event.arguments === "string") mapped.arguments = await transform(event.arguments);
2020
+ if (typeof event.output === "string") mapped.output = await transform(event.output);
2021
+ return mapped;
2022
+ })
2023
+ );
2024
+ }
2025
+ function encryptToolCallEvents(events, address, signMessage, embeddedWalletSigner) {
2026
+ return mapToolCallPayloads(
2027
+ events,
2028
+ (value) => encryptField(value, address, signMessage, embeddedWalletSigner, true)
2029
+ );
2030
+ }
1973
2031
  async function decryptMaybeJsonFieldDetailed(value, address) {
1974
2032
  if (value === void 0) return { value: void 0, status: "plaintext" };
1975
2033
  if (value === null) return { value, status: "plaintext" };
@@ -2055,6 +2113,10 @@ async function assembleDecryptedMessage(message, address) {
2055
2113
  message.thoughtProcess,
2056
2114
  address
2057
2115
  );
2116
+ const toolCallEventsResult = await decryptMaybeJsonFieldDetailed(
2117
+ message.toolCallEvents,
2118
+ address
2119
+ );
2058
2120
  let decryptionStatus;
2059
2121
  let refreshProbe;
2060
2122
  const consider = (status, probe) => {
@@ -2073,6 +2135,14 @@ async function assembleDecryptedMessage(message, address) {
2073
2135
  if (thoughtProcessResult.probe) {
2074
2136
  consider(thoughtProcessResult.status, thoughtProcessResult.probe);
2075
2137
  }
2138
+ if (toolCallEventsResult.probe) {
2139
+ consider(toolCallEventsResult.status, toolCallEventsResult.probe);
2140
+ }
2141
+ const toolCallEvents = Array.isArray(toolCallEventsResult.value) ? await mapToolCallPayloads(toolCallEventsResult.value, async (value) => {
2142
+ const detailed = await decryptFieldDetailed(value, address);
2143
+ consider(detailed.status, isEncrypted(value) ? value : void 0);
2144
+ return detailed.value;
2145
+ }) : void 0;
2076
2146
  const statusForConsumer = decryptionStatus === "ok" || decryptionStatus === "plaintext" || !decryptionStatus ? void 0 : decryptionStatus;
2077
2147
  const result = {
2078
2148
  ...message,
@@ -2081,7 +2151,8 @@ async function assembleDecryptedMessage(message, address) {
2081
2151
  vector: vectorResult.value,
2082
2152
  chunks: chunksResult.value,
2083
2153
  sources: sourcesResult.value,
2084
- thoughtProcess: thoughtProcessResult.value
2154
+ thoughtProcess: thoughtProcessResult.value,
2155
+ toolCallEvents
2085
2156
  };
2086
2157
  if (statusForConsumer) {
2087
2158
  result.decryptionStatus = statusForConsumer;
@@ -2875,7 +2946,7 @@ async function updateMessageChunksOp(ctx, uniqueId, chunks3, embeddingModel) {
2875
2946
  });
2876
2947
  return messageToStored(message, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner);
2877
2948
  }
2878
- var STRIP_CHUNK_TEXT_PAGE_SIZE = 100;
2949
+ var RAW_REPAIR_PAGE_SIZE = 100;
2879
2950
  function chunksWithoutText(raw) {
2880
2951
  if (typeof raw !== "string") return null;
2881
2952
  let parsed;
@@ -2898,8 +2969,8 @@ function chunksWithoutText(raw) {
2898
2969
  async function stripLegacyChunkTextOp(ctx) {
2899
2970
  const candidateIds = await ctx.messagesCollection.query(Q.where("chunks", Q.like(`%${Q.sanitizeLikeString('"text":')}%`))).fetchIds();
2900
2971
  let stripped = 0;
2901
- for (let start = 0; start < candidateIds.length; start += STRIP_CHUNK_TEXT_PAGE_SIZE) {
2902
- const pageIds = candidateIds.slice(start, start + STRIP_CHUNK_TEXT_PAGE_SIZE);
2972
+ for (let start = 0; start < candidateIds.length; start += RAW_REPAIR_PAGE_SIZE) {
2973
+ const pageIds = candidateIds.slice(start, start + RAW_REPAIR_PAGE_SIZE);
2903
2974
  const rows = await ctx.messagesCollection.query(Q.where("id", Q.oneOf(pageIds))).unsafeFetchRaw();
2904
2975
  const changedIds = rows.filter((row) => chunksWithoutText(row.chunks) !== null).map((row) => String(row.id));
2905
2976
  if (changedIds.length === 0) continue;
@@ -2923,6 +2994,88 @@ async function stripLegacyChunkTextOp(ctx) {
2923
2994
  }
2924
2995
  return stripped;
2925
2996
  }
2997
+ async function encryptedToolCallEventsColumn(raw, address, signMessage, embeddedWalletSigner) {
2998
+ if (typeof raw !== "string") return null;
2999
+ let parsed;
3000
+ try {
3001
+ parsed = JSON.parse(raw);
3002
+ } catch {
3003
+ return null;
3004
+ }
3005
+ if (!Array.isArray(parsed)) return null;
3006
+ const column = JSON.stringify(
3007
+ await encryptToolCallEvents(
3008
+ parsed,
3009
+ address,
3010
+ signMessage,
3011
+ embeddedWalletSigner
3012
+ )
3013
+ );
3014
+ return column === JSON.stringify(parsed) ? null : column;
3015
+ }
3016
+ async function encryptToolCallEventsOp(ctx) {
3017
+ const { walletAddress: address, signMessage, embeddedWalletSigner } = ctx;
3018
+ if (!address || !signMessage) return 0;
3019
+ if (!await requestEncryptionKey(address, signMessage, embeddedWalletSigner)) return 0;
3020
+ if (!hasEncryptionKey(address)) return 0;
3021
+ if (!await savedKeyOpensExistingData(ctx, address, signMessage, embeddedWalletSigner)) {
3022
+ return 0;
3023
+ }
3024
+ const candidateIds = await ctx.messagesCollection.query(
3025
+ Q.where("tool_call_events", Q.notEq(null)),
3026
+ Q.where("tool_call_events", Q.notIn(["", "[]"]))
3027
+ ).fetchIds();
3028
+ let encrypted = 0;
3029
+ for (let start = 0; start < candidateIds.length; start += RAW_REPAIR_PAGE_SIZE) {
3030
+ const pageIds = candidateIds.slice(start, start + RAW_REPAIR_PAGE_SIZE);
3031
+ const rows = await ctx.messagesCollection.query(Q.where("id", Q.oneOf(pageIds))).unsafeFetchRaw();
3032
+ const repairs = /* @__PURE__ */ new Map();
3033
+ for (const row of rows) {
3034
+ const column = await encryptedToolCallEventsColumn(
3035
+ row.tool_call_events,
3036
+ address,
3037
+ signMessage,
3038
+ embeddedWalletSigner
3039
+ );
3040
+ if (column !== null) repairs.set(String(row.id), { raw: row.tool_call_events, column });
3041
+ }
3042
+ if (repairs.size === 0) continue;
3043
+ await ctx.database.write(async () => {
3044
+ const messages = (await ctx.messagesCollection.query(Q.where("id", Q.oneOf([...repairs.keys()]))).fetch()).filter(
3045
+ (message) => message._raw.tool_call_events === repairs.get(message.id)?.raw
3046
+ );
3047
+ if (messages.length === 0) return;
3048
+ const conversationIds = [...new Set(messages.map((message) => message.conversationId))];
3049
+ const conversations = await ctx.conversationsCollection.query(Q.where("conversation_id", Q.oneOf(conversationIds))).fetch();
3050
+ await ctx.database.batch(
3051
+ ...messages.map(
3052
+ (message) => message.prepareUpdate((msg) => {
3053
+ msg._setRaw("tool_call_events", repairs.get(message.id).column);
3054
+ })
3055
+ ),
3056
+ ...conversations.map((conversation) => conversation.prepareUpdate(() => {
3057
+ }))
3058
+ );
3059
+ encrypted += messages.length;
3060
+ });
3061
+ }
3062
+ return encrypted;
3063
+ }
3064
+ async function savedKeyOpensExistingData(ctx, address, signMessage, embeddedWalletSigner) {
3065
+ const v3Probe = await findEncryptedContent(ctx, "enc:v3:%");
3066
+ if (v3Probe) {
3067
+ const { status } = await decryptFieldDetailed(v3Probe, address);
3068
+ if (status !== "auth_mismatch" && status !== "key_missing") return true;
3069
+ return refreshEncryptionKeyIfMatches(address, v3Probe, signMessage, embeddedWalletSigner);
3070
+ }
3071
+ const v2Probe = await findEncryptedContent(ctx, "enc:v2:%");
3072
+ if (!v2Probe) return true;
3073
+ return refreshEncryptionKeyIfMatches(address, v2Probe, signMessage, embeddedWalletSigner);
3074
+ }
3075
+ async function findEncryptedContent(ctx, pattern) {
3076
+ const [row] = await ctx.messagesCollection.query(Q.where("content", Q.like(pattern)), Q.take(1)).unsafeFetchRaw();
3077
+ return row ? String(row.content) : null;
3078
+ }
2926
3079
  async function updateMessageErrorOp(ctx, uniqueId, error) {
2927
3080
  let message;
2928
3081
  try {
@@ -3069,6 +3222,95 @@ async function searchMessagesOp(ctx, queryVector, options) {
3069
3222
  })
3070
3223
  );
3071
3224
  }
3225
+ var CHUNK_VECTOR_BATCH_SIZE = 50;
3226
+ var CHUNK_SCAN_YIELD_BUDGET_MS = 12;
3227
+ var hasEmbeddingSql = `(coalesce("chunks", '') != '' or coalesce("vector", '') != '')`;
3228
+ function toChunkCandidateKey(raw) {
3229
+ return {
3230
+ id: raw.id,
3231
+ conversationId: String(raw.conversation_id),
3232
+ version: Number(raw.updated_at) || 0,
3233
+ embeddingModel: raw.embedding_model ?? null
3234
+ };
3235
+ }
3236
+ async function fetchRowsByIds(ctx, ids) {
3237
+ if (ids.length === 0) return [];
3238
+ return await ctx.messagesCollection.query(Q.where("id", Q.oneOf(ids))).unsafeFetchRaw();
3239
+ }
3240
+ async function projectedChunkRowSource(ctx, filter) {
3241
+ const clauses = [
3242
+ `"history"."_status" is not 'deleted'`,
3243
+ hasEmbeddingSql,
3244
+ `exists (select 1 from "conversations" where "conversations"."conversation_id" = "history"."conversation_id" and "conversations"."is_deleted" = 0 and "conversations"."_status" is not 'deleted')`
3245
+ ];
3246
+ const args = [];
3247
+ if (filter.conversationId !== void 0) {
3248
+ clauses.push(`"history"."conversation_id" = ?`);
3249
+ args.push(filter.conversationId);
3250
+ }
3251
+ if (filter.excludeConversationId !== void 0) {
3252
+ clauses.push(`"history"."conversation_id" != ?`);
3253
+ args.push(filter.excludeConversationId);
3254
+ }
3255
+ const keyRows = await ctx.messagesCollection.query(
3256
+ Q.unsafeSqlQuery(
3257
+ `select "id", "conversation_id", "updated_at", "embedding_model" from "history" where ${clauses.join(" and ")}`,
3258
+ args
3259
+ )
3260
+ ).unsafeFetchRaw();
3261
+ return {
3262
+ keys: keyRows.map(toChunkCandidateKey),
3263
+ loadVectorRows: async (ids) => {
3264
+ if (ids.length === 0) return [];
3265
+ return await ctx.messagesCollection.query(
3266
+ Q.unsafeSqlQuery(
3267
+ `select "id", "updated_at", "chunks", "vector" from "history" where "_status" is not 'deleted' and "id" in (${ids.map(() => "?").join(",")})`,
3268
+ ids
3269
+ )
3270
+ ).unsafeFetchRaw();
3271
+ },
3272
+ loadFullRows: (ids) => fetchRowsByIds(ctx, ids)
3273
+ };
3274
+ }
3275
+ async function fullScanChunkRowSource(ctx, filter) {
3276
+ const activeConversations = await ctx.conversationsCollection.query(Q.where("is_deleted", false)).fetch();
3277
+ const activeConversationIds = new Set(
3278
+ activeConversations.map((c) => String(c._getRaw("conversation_id")))
3279
+ );
3280
+ const queryConditions = filter.conversationId !== void 0 ? [Q.where("conversation_id", filter.conversationId)] : [];
3281
+ const rows = await ctx.messagesCollection.query(...queryConditions).unsafeFetchRaw();
3282
+ const rowsById = /* @__PURE__ */ new Map();
3283
+ for (const row of rows) {
3284
+ const conversationId = String(row.conversation_id);
3285
+ if (!activeConversationIds.has(conversationId)) continue;
3286
+ if (filter.excludeConversationId !== void 0 && conversationId === filter.excludeConversationId) {
3287
+ continue;
3288
+ }
3289
+ if (!row.chunks && !row.vector) continue;
3290
+ rowsById.set(row.id, row);
3291
+ }
3292
+ const pick = (ids) => Promise.resolve(
3293
+ ids.flatMap((id) => {
3294
+ const row = rowsById.get(id);
3295
+ return row ? [row] : [];
3296
+ })
3297
+ );
3298
+ return {
3299
+ keys: Array.from(rowsById.values(), toChunkCandidateKey),
3300
+ loadVectorRows: pick,
3301
+ loadFullRows: pick
3302
+ };
3303
+ }
3304
+ async function chunkRowSource(ctx, filter) {
3305
+ try {
3306
+ return await projectedChunkRowSource(ctx, filter);
3307
+ } catch (err) {
3308
+ getLogger().debug(
3309
+ "searchChunksOp: projected SQL unavailable, using full-load fallback: " + (err instanceof Error ? err.message : String(err))
3310
+ );
3311
+ return fullScanChunkRowSource(ctx, filter);
3312
+ }
3313
+ }
3072
3314
  async function searchChunksOp(ctx, queryVector, options) {
3073
3315
  const {
3074
3316
  limit = 10,
@@ -3076,83 +3318,111 @@ async function searchChunksOp(ctx, queryVector, options) {
3076
3318
  conversationId,
3077
3319
  excludeConversationId,
3078
3320
  embeddingModel,
3079
- chunkCache
3321
+ chunkCache,
3322
+ onStats
3080
3323
  } = options || {};
3081
- const activeConversations = await ctx.conversationsCollection.query(Q.where("is_deleted", false)).fetch();
3082
- const activeConversationIds = new Set(
3083
- activeConversations.map((c) => String(c._getRaw("conversation_id")))
3084
- );
3085
- const queryConditions = conversationId ? [Q.where("conversation_id", conversationId)] : [];
3086
- const messages = await ctx.messagesCollection.query(...queryConditions).unsafeFetchRaw();
3324
+ const source = await chunkRowSource(ctx, { conversationId, excludeConversationId });
3325
+ const yieldIfDue = createTimeSliceYielder(CHUNK_SCAN_YIELD_BUDGET_MS);
3087
3326
  const candidates = [];
3088
3327
  let staleSkipped = 0;
3089
3328
  let unreadableVectors = 0;
3090
- for (const message of messages) {
3091
- const msgConvId = String(message.conversation_id);
3092
- if (!activeConversationIds.has(msgConvId)) continue;
3093
- if (excludeConversationId !== void 0 && msgConvId === excludeConversationId) continue;
3094
- if (embeddingModel) {
3095
- const storedModel = message.embedding_model;
3096
- if (storedModel !== null && storedModel !== void 0 && storedModel !== embeddingModel) {
3097
- staleSkipped++;
3098
- continue;
3099
- }
3329
+ let cacheHits = 0;
3330
+ const missIds = [];
3331
+ const orderById = /* @__PURE__ */ new Map();
3332
+ for (const [order, key] of source.keys.entries()) {
3333
+ await yieldIfDue();
3334
+ orderById.set(key.id, order);
3335
+ if (embeddingModel && key.embeddingModel !== null && key.embeddingModel !== embeddingModel) {
3336
+ staleSkipped++;
3337
+ continue;
3100
3338
  }
3101
- const version = Number(message.updated_at) || 0;
3102
- const cached = chunkCache?.get(message.id);
3103
- if (cached && cached.version === version) {
3104
- if (cached.chunks.length > 0) {
3105
- for (let ci = 0; ci < cached.chunks.length; ci++) {
3106
- const vec = cached.chunks[ci];
3107
- if (vec.length === 0) continue;
3108
- const similarity = cosineSimilarity(queryVector, vec);
3109
- if (similarity >= minSimilarity) {
3110
- candidates.push({
3111
- message,
3112
- similarity,
3113
- chunkTextSource: { kind: "chunk-cached", chunkIndex: ci, version }
3114
- });
3115
- }
3116
- }
3117
- } else if (cached.fallback) {
3118
- const similarity = cosineSimilarity(queryVector, cached.fallback);
3119
- if (similarity >= minSimilarity) {
3120
- candidates.push({ message, similarity, chunkTextSource: { kind: "message" } });
3121
- }
3122
- }
3339
+ const cached = chunkCache?.get(key.id);
3340
+ if (!cached || cached.version !== key.version) {
3341
+ missIds.push(key.id);
3123
3342
  continue;
3124
3343
  }
3125
- const chunks3 = await readRawJsonField(message, "chunks", ctx.walletAddress);
3126
- if (chunks3 && chunks3.length > 0) {
3127
- const vectors = chunks3.map(
3128
- (chunk) => decodeChunkVector(chunk.vector, () => {
3129
- unreadableVectors++;
3130
- })
3131
- );
3132
- chunkCache?.set(message.id, { version, chunks: vectors });
3133
- for (let ci = 0; ci < chunks3.length; ci++) {
3134
- const vec = vectors[ci];
3344
+ cacheHits++;
3345
+ if (cached.chunks.length > 0) {
3346
+ for (let ci = 0; ci < cached.chunks.length; ci++) {
3347
+ const vec = cached.chunks[ci];
3135
3348
  if (vec.length === 0) continue;
3136
3349
  const similarity = cosineSimilarity(queryVector, vec);
3137
3350
  if (similarity >= minSimilarity) {
3138
3351
  candidates.push({
3139
- message,
3352
+ id: key.id,
3353
+ version: key.version,
3354
+ order,
3140
3355
  similarity,
3141
- chunkTextSource: { kind: "chunk", chunk: chunks3[ci], siblings: chunks3 }
3356
+ chunkTextSource: { kind: "chunk-cached", chunkIndex: ci, version: key.version }
3142
3357
  });
3143
3358
  }
3144
3359
  }
3145
- } else {
3146
- const messageVector = await readRawJsonField(message, "vector", ctx.walletAddress);
3147
- if (!messageVector || messageVector.length === 0) continue;
3148
- const fallback = Float32Array.from(messageVector);
3149
- chunkCache?.set(message.id, { version, chunks: [], fallback });
3150
- const similarity = cosineSimilarity(queryVector, fallback);
3360
+ } else if (cached.fallback) {
3361
+ const similarity = cosineSimilarity(queryVector, cached.fallback);
3151
3362
  if (similarity >= minSimilarity) {
3152
- candidates.push({ message, similarity, chunkTextSource: { kind: "message" } });
3363
+ candidates.push({
3364
+ id: key.id,
3365
+ version: key.version,
3366
+ order,
3367
+ similarity,
3368
+ chunkTextSource: { kind: "message" }
3369
+ });
3370
+ }
3371
+ }
3372
+ }
3373
+ let rowsFetched = 0;
3374
+ for (let offset = 0; offset < missIds.length; offset += CHUNK_VECTOR_BATCH_SIZE) {
3375
+ const rows = await source.loadVectorRows(
3376
+ missIds.slice(offset, offset + CHUNK_VECTOR_BATCH_SIZE)
3377
+ );
3378
+ rowsFetched += rows.length;
3379
+ for (const row of rows) {
3380
+ await yieldIfDue();
3381
+ const id = row.id;
3382
+ const version = Number(row.updated_at) || 0;
3383
+ const order = orderById.get(id) ?? Number.MAX_SAFE_INTEGER;
3384
+ const chunks3 = await readRawJsonField(row, "chunks", ctx.walletAddress);
3385
+ if (chunks3 && chunks3.length > 0) {
3386
+ const vectors = chunks3.map(
3387
+ (chunk) => decodeChunkVector(chunk.vector, () => {
3388
+ unreadableVectors++;
3389
+ })
3390
+ );
3391
+ chunkCache?.set(id, { version, chunks: vectors });
3392
+ for (let ci = 0; ci < chunks3.length; ci++) {
3393
+ const vec = vectors[ci];
3394
+ if (vec.length === 0) continue;
3395
+ const similarity = cosineSimilarity(queryVector, vec);
3396
+ if (similarity >= minSimilarity) {
3397
+ candidates.push({
3398
+ id,
3399
+ version,
3400
+ order,
3401
+ similarity,
3402
+ chunkTextSource: { kind: "chunk", chunk: chunks3[ci], siblings: chunks3 }
3403
+ });
3404
+ }
3405
+ }
3406
+ } else {
3407
+ const messageVector = await readRawJsonField(row, "vector", ctx.walletAddress);
3408
+ if (messageVector && messageVector.length > 0) {
3409
+ const fallback = Float32Array.from(messageVector);
3410
+ chunkCache?.set(id, { version, chunks: [], fallback });
3411
+ const similarity = cosineSimilarity(queryVector, fallback);
3412
+ if (similarity >= minSimilarity) {
3413
+ candidates.push({
3414
+ id,
3415
+ version,
3416
+ order,
3417
+ similarity,
3418
+ chunkTextSource: { kind: "message" }
3419
+ });
3420
+ }
3421
+ }
3153
3422
  }
3154
3423
  }
3155
3424
  }
3425
+ onStats?.({ rowsScanned: source.keys.length, rowsFetched, cacheHits });
3156
3426
  if (staleSkipped > 0) {
3157
3427
  getLogger().warn(
3158
3428
  `searchChunksOp: skipped ${staleSkipped} messages whose embedding model differs from the current model (${embeddingModel}) \u2014 re-embed via chunkAndEmbedAllMessages`
@@ -3163,12 +3433,17 @@ async function searchChunksOp(ctx, queryVector, options) {
3163
3433
  `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`
3164
3434
  );
3165
3435
  }
3166
- const topK = candidates.sort((a, b) => b.similarity - a.similarity).slice(0, limit);
3167
- const uniqueMessages = /* @__PURE__ */ new Map();
3168
- for (const c of topK) uniqueMessages.set(c.message.id, c.message);
3436
+ const topK = candidates.sort((a, b) => b.similarity - a.similarity || a.order - b.order).slice(0, limit);
3437
+ const topIds = Array.from(new Set(topK.map((c) => c.id)));
3438
+ const fullRows = await source.loadFullRows(topIds);
3439
+ const rowById = new Map(fullRows.map((row) => [row.id, row]));
3440
+ const liveTopK = topK.filter((c) => {
3441
+ const row = rowById.get(c.id);
3442
+ return row !== void 0 && (Number(row.updated_at) || 0) === c.version;
3443
+ });
3169
3444
  const storedById = new Map(
3170
3445
  await Promise.all(
3171
- Array.from(uniqueMessages, async ([id, m]) => {
3446
+ Array.from(rowById, async ([id, m]) => {
3172
3447
  const stored2 = await messageRawToStored(
3173
3448
  m,
3174
3449
  ctx.walletAddress,
@@ -3181,24 +3456,24 @@ async function searchChunksOp(ctx, queryVector, options) {
3181
3456
  );
3182
3457
  const chunkStateByMsgId = /* @__PURE__ */ new Map();
3183
3458
  const needsChunkText = /* @__PURE__ */ new Set();
3184
- for (const c of topK) {
3185
- if (c.chunkTextSource.kind === "chunk-cached") needsChunkText.add(c.message.id);
3459
+ for (const c of liveTopK) {
3460
+ if (c.chunkTextSource.kind === "chunk-cached") needsChunkText.add(c.id);
3186
3461
  }
3187
3462
  await Promise.all(
3188
3463
  Array.from(needsChunkText, async (id) => {
3189
- const m = uniqueMessages.get(id);
3464
+ const m = rowById.get(id);
3190
3465
  const parsed = await readRawJsonField(m, "chunks", ctx.walletAddress);
3191
3466
  const version = Number(m.updated_at) || 0;
3192
3467
  if (parsed) chunkStateByMsgId.set(id, { version, chunks: parsed });
3193
3468
  })
3194
3469
  );
3195
- return topK.map(({ message, similarity, chunkTextSource }) => {
3196
- const stored2 = { ...storedById.get(message.id) };
3470
+ return liveTopK.map(({ id, similarity, chunkTextSource }) => {
3471
+ const stored2 = { ...storedById.get(id) };
3197
3472
  let chunkText2;
3198
3473
  if (chunkTextSource.kind === "chunk") {
3199
3474
  chunkText2 = resolveChunkText(stored2.content, chunkTextSource.chunk, chunkTextSource.siblings);
3200
3475
  } else if (chunkTextSource.kind === "chunk-cached") {
3201
- const state = chunkStateByMsgId.get(message.id);
3476
+ const state = chunkStateByMsgId.get(id);
3202
3477
  const chunk = state && state.version === chunkTextSource.version ? state.chunks[chunkTextSource.chunkIndex] : void 0;
3203
3478
  chunkText2 = resolveChunkText(stored2.content, chunk, state?.chunks);
3204
3479
  } else {
@@ -4352,13 +4627,35 @@ async function generateEmbedding(text14, options) {
4352
4627
  if (total === void 0) return embedOne(text14, options, void 0);
4353
4628
  return withDeadline((signal) => embedOne(text14, options, signal), total, "embedding");
4354
4629
  }
4630
+ var inFlightByCache = /* @__PURE__ */ new WeakMap();
4355
4631
  async function embedOne(text14, options, outer) {
4356
- const { baseUrl = BASE_URL, model, cache } = options;
4357
- const timeoutMs = options.timeoutMs ?? DEFAULT_EMBEDDING_REQUEST_TIMEOUT_MS;
4358
- if (cache) {
4359
- const cached = cache.get(text14);
4360
- if (cached) return Array.from(cached);
4632
+ const { cache } = options;
4633
+ if (!cache) return Array.from(await requestEmbedding(text14, options, outer));
4634
+ const cached = cache.get(text14);
4635
+ if (cached) return Array.from(cached);
4636
+ const inFlight = inFlightByCache.get(cache) ?? /* @__PURE__ */ new Map();
4637
+ inFlightByCache.set(cache, inFlight);
4638
+ const pending = inFlight.get(text14);
4639
+ if (pending) {
4640
+ try {
4641
+ return Array.from(await pending);
4642
+ } catch (err) {
4643
+ if (outer?.aborted) throw err;
4644
+ }
4361
4645
  }
4646
+ const request = requestEmbedding(text14, options, outer);
4647
+ inFlight.set(text14, request);
4648
+ try {
4649
+ const embedding2 = await request;
4650
+ cache.set(text14, embedding2);
4651
+ return Array.from(embedding2);
4652
+ } finally {
4653
+ if (inFlight.get(text14) === request) inFlight.delete(text14);
4654
+ }
4655
+ }
4656
+ async function requestEmbedding(text14, options, outer) {
4657
+ const { baseUrl = BASE_URL, model } = options;
4658
+ const timeoutMs = options.timeoutMs ?? DEFAULT_EMBEDDING_REQUEST_TIMEOUT_MS;
4362
4659
  const headers = {
4363
4660
  ...await resolveAuthHeaders(options, outer),
4364
4661
  ...taskTypeHeader(options.taskType)
@@ -4393,11 +4690,7 @@ async function embedOne(text14, options, outer) {
4393
4690
  totalTokens: response.data.usage.total_tokens ?? 0
4394
4691
  });
4395
4692
  }
4396
- const f32Embedding = Float32Array.from(embedding2);
4397
- if (cache) {
4398
- cache.set(text14, f32Embedding);
4399
- }
4400
- return Array.from(f32Embedding);
4693
+ return Float32Array.from(embedding2);
4401
4694
  }
4402
4695
  var DEFAULT_EMBEDDING_BATCH_SIZE = 100;
4403
4696
  var DEFAULT_EMBEDDING_BATCH_CONCURRENCY = 3;
@@ -4795,7 +5088,8 @@ function extractReservationReceipts(toolCallEvents) {
4795
5088
  for (const event of toolCallEvents ?? []) {
4796
5089
  const action = ACTIONS.find((a) => event.name?.endsWith(a.tool));
4797
5090
  const output = event.output ?? "";
4798
- if (!action || !output || NOT_AN_ATTEMPT.test(output)) continue;
5091
+ if (!action || !output || hasEncryptedToolPayload(event) || NOT_AN_ATTEMPT.test(output))
5092
+ continue;
4799
5093
  const kept = latest.get(action.kind)?.status;
4800
5094
  if (kept === "made") continue;
4801
5095
  const receipt = toReceipt(action, event.arguments ?? "", output);
@@ -15676,9 +15970,9 @@ function recencyMultiplier(updatedAt, opts = {}) {
15676
15970
  const updatedMs = updatedAt.getTime();
15677
15971
  if (!Number.isFinite(updatedMs)) return noDate;
15678
15972
  const now = opts.now ?? /* @__PURE__ */ new Date();
15679
- const nowMs3 = now.getTime();
15680
- if (!Number.isFinite(nowMs3)) return noDate;
15681
- const ageDays2 = Math.max(0, (nowMs3 - updatedMs) / DAY_MS2);
15973
+ const nowMs2 = now.getTime();
15974
+ if (!Number.isFinite(nowMs2)) return noDate;
15975
+ const ageDays2 = Math.max(0, (nowMs2 - updatedMs) / DAY_MS2);
15682
15976
  const perYearDecay = opts.perYearDecay ?? DEFAULT_PER_YEAR_DECAY;
15683
15977
  const floor = opts.floor ?? DEFAULT_FLOOR;
15684
15978
  const decay = perYearDecay * (ageDays2 / 365);
@@ -16187,9 +16481,6 @@ function createVaultEmbeddingCache(maxSize = DEFAULT_VAULT_CACHE_SIZE) {
16187
16481
 
16188
16482
  // src/lib/memoryVault/searchTool.ts
16189
16483
  var DEFAULT_RERANK_TOP_N = 5;
16190
- function nowMs() {
16191
- return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
16192
- }
16193
16484
  var warnedProgrammaticDecomposeIgnored = false;
16194
16485
  function normalizeEventTimeKind(kind) {
16195
16486
  return kind === "point" || kind === "range" || kind === "ongoing" ? kind : null;
@@ -19080,6 +19371,32 @@ function rankProfileCandidates(inputs, now = Date.now(), options = {}) {
19080
19371
  return ranked;
19081
19372
  }
19082
19373
 
19374
+ // src/lib/db/chat/embeddingCache.ts
19375
+ var MaskScopedEmbeddingCache = class extends Map {
19376
+ constructor(inner, masked) {
19377
+ super();
19378
+ this.inner = inner;
19379
+ this.masked = masked;
19380
+ }
19381
+ key(text14) {
19382
+ return `${this.masked ? "m" : "r"}:${text14}`;
19383
+ }
19384
+ get(text14) {
19385
+ return this.inner.get(this.key(text14));
19386
+ }
19387
+ set(text14, value) {
19388
+ this.inner.set(this.key(text14), value);
19389
+ return this;
19390
+ }
19391
+ };
19392
+ var scopedViews = /* @__PURE__ */ new WeakMap();
19393
+ function maskScopedEmbeddingCache(cache, masked) {
19394
+ const views = scopedViews.get(cache) ?? {};
19395
+ scopedViews.set(cache, views);
19396
+ const slot = masked ? "masked" : "raw";
19397
+ return views[slot] ?? (views[slot] = new MaskScopedEmbeddingCache(cache, masked));
19398
+ }
19399
+
19083
19400
  // src/lib/memory/queryTemporal.ts
19084
19401
  var RELATIVE_DAY_ENTRIES = [
19085
19402
  [/\btoday\b/, "today", 0],
@@ -19287,9 +19604,6 @@ function scoreEventTimeOverlap(memoryStart, memoryEnd, memoryKind, window2) {
19287
19604
  var DEFAULT_LIMIT = 8;
19288
19605
  var DEFAULT_BUDGET = "low";
19289
19606
  var DEFAULT_FACT_MIN_SCORE = 0.1;
19290
- function nowMs2() {
19291
- return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
19292
- }
19293
19607
  var DEFAULT_CHUNK_MIN_SCORE = 0.5;
19294
19608
  var DEFAULT_QUERY_EMBED_TOTAL_TIMEOUT_MS = 8e3;
19295
19609
  function flagsForBudget(budget) {
@@ -19327,12 +19641,19 @@ async function recall(query, ctx, options = {}) {
19327
19641
  const subQueries = normalizedFacets.length >= 2 ? normalizedFacets : void 0;
19328
19642
  const embeddingOptions = {
19329
19643
  ...ctx.embeddingOptions,
19330
- taskType: "memory_recall_embed"
19644
+ taskType: "memory_recall_embed",
19645
+ ...options.embeddingCache && !ctx.embeddingOptions.cache && {
19646
+ cache: maskScopedEmbeddingCache(
19647
+ options.embeddingCache,
19648
+ Boolean(ctx.embeddingOptions.maskInput)
19649
+ )
19650
+ }
19331
19651
  };
19332
- const t0 = nowMs2();
19652
+ const t0 = nowMs();
19333
19653
  let prepMs = 0;
19334
19654
  let factLaneMs = 0;
19335
19655
  let chunkLaneMs = 0;
19656
+ let chunkStats;
19336
19657
  let fuseMs = 0;
19337
19658
  const factResults = [];
19338
19659
  const chunkResults = [];
@@ -19387,6 +19708,11 @@ async function recall(query, ctx, options = {}) {
19387
19708
  ...decryptLastRan !== void 0 && { decryptLast: decryptLastRan },
19388
19709
  ...vaultRowsDecrypted !== void 0 && { vaultRowsDecrypted },
19389
19710
  ...vaultRowsEmbedded !== void 0 && { vaultRowsEmbedded },
19711
+ ...chunkStats && {
19712
+ chunkRowsScanned: chunkStats.rowsScanned,
19713
+ chunkRowsFetched: chunkStats.rowsFetched,
19714
+ chunkCacheHits: chunkStats.cacheHits
19715
+ },
19390
19716
  factCount: factResults.length,
19391
19717
  chunkCount: chunkResults.length,
19392
19718
  admittedCount: admitted.length,
@@ -19398,7 +19724,7 @@ async function recall(query, ctx, options = {}) {
19398
19724
  temporalLaneCount,
19399
19725
  emptyReason: emptyReasonFor(admitted.length, laneRan),
19400
19726
  timings: {
19401
- total: nowMs2() - t0,
19727
+ total: nowMs() - t0,
19402
19728
  prep: prepMs,
19403
19729
  factLane: factLaneMs,
19404
19730
  rerank: rerankMs,
@@ -19420,14 +19746,14 @@ async function recall(query, ctx, options = {}) {
19420
19746
  const needsChunkEmbedding = types.includes("chunk") && ctx.storageCtx;
19421
19747
  const wantsTemporal = types.includes("fact") && (ctx.factSource || ctx.vaultCtx);
19422
19748
  const queryEmbedTotalTimeoutMs = options.queryEmbedTotalTimeoutMs ?? DEFAULT_QUERY_EMBED_TOTAL_TIMEOUT_MS;
19423
- const prepStart = nowMs2();
19749
+ const prepStart = nowMs();
19424
19750
  let sharedEmbedMs = 0;
19425
19751
  const [queryEmbedding, entityRanking, temporalRanking] = await Promise.all([
19426
19752
  needsChunkEmbedding ? generateEmbedding(query, {
19427
19753
  ...embeddingOptions,
19428
19754
  totalTimeoutMs: queryEmbedTotalTimeoutMs
19429
19755
  }).finally(() => {
19430
- sharedEmbedMs = nowMs2() - prepStart;
19756
+ sharedEmbedMs = nowMs() - prepStart;
19431
19757
  }).then((vec) => {
19432
19758
  if (vec.length === 0) {
19433
19759
  getLogger().warn(
@@ -19460,11 +19786,11 @@ async function recall(query, ctx, options = {}) {
19460
19786
  () => ctx.factSource ? ctx.factSource.temporalRanking(query, options.now) : buildTemporalLaneRanking(query, ctx.vaultCtx, options.now)
19461
19787
  ) : Promise.resolve([])
19462
19788
  ]);
19463
- prepMs = nowMs2() - prepStart;
19789
+ prepMs = nowMs() - prepStart;
19464
19790
  graphLaneCount = entityRanking.length;
19465
19791
  temporalLaneCount = temporalRanking.length;
19466
19792
  if (types.includes("fact") && (ctx.factSource || ctx.vaultCtx && ctx.vaultCache)) {
19467
- const factStart = nowMs2();
19793
+ const factStart = nowMs();
19468
19794
  const vaultMinScore = options.minScore ?? DEFAULT_FACT_MIN_SCORE;
19469
19795
  factFloor = vaultMinScore;
19470
19796
  const {
@@ -19540,11 +19866,11 @@ async function recall(query, ctx, options = {}) {
19540
19866
  factLaneRankedOnCosine = factRankedOnCosine;
19541
19867
  decryptLastRan = factDecryptLast;
19542
19868
  vaultRowsDecrypted = factRowsDecrypted;
19543
- factLaneMs = nowMs2() - factStart;
19869
+ factLaneMs = nowMs() - factStart;
19544
19870
  }
19545
19871
  if (chunkEmbedFailed && !factLaneRankedOnCosine) embeddingsUnavailable = true;
19546
19872
  if (types.includes("chunk") && ctx.storageCtx && queryEmbedding) {
19547
- const chunkStart = nowMs2();
19873
+ const chunkStart = nowMs();
19548
19874
  const chunkMinScore = options.minScore ?? DEFAULT_CHUNK_MIN_SCORE;
19549
19875
  chunkFloor = chunkMinScore;
19550
19876
  const results = await searchChunksOp(ctx.storageCtx, queryEmbedding, {
@@ -19555,12 +19881,15 @@ async function recall(query, ctx, options = {}) {
19555
19881
  ...options.excludeConversationId && {
19556
19882
  excludeConversationId: options.excludeConversationId
19557
19883
  },
19558
- ...ctx.chunkCache && { chunkCache: ctx.chunkCache }
19884
+ ...ctx.chunkCache && { chunkCache: ctx.chunkCache },
19885
+ onStats: (stats) => {
19886
+ chunkStats = stats;
19887
+ }
19559
19888
  });
19560
19889
  chunkResults.push(...dedupeBy(results, (r) => r.chunkText.trim()));
19561
- chunkLaneMs = nowMs2() - chunkStart;
19890
+ chunkLaneMs = nowMs() - chunkStart;
19562
19891
  }
19563
- const fuseStart = nowMs2();
19892
+ const fuseStart = nowMs();
19564
19893
  if (types.length === 1 || factResults.length === 0 || chunkResults.length === 0) {
19565
19894
  const memories2 = [
19566
19895
  ...factResults.map((r) => toFactMemory(r, options.now)),
@@ -19568,7 +19897,7 @@ async function recall(query, ctx, options = {}) {
19568
19897
  ];
19569
19898
  memories2.sort((a, b) => b.score - a.score);
19570
19899
  const candidateCount = factResults.length + chunkResults.length;
19571
- fuseMs = nowMs2() - fuseStart;
19900
+ fuseMs = nowMs() - fuseStart;
19572
19901
  const admitted = memories2.slice(0, limit);
19573
19902
  hitLimit = memories2.length > limit;
19574
19903
  emitDiagnostics(candidateCount, admitted);
@@ -19632,7 +19961,7 @@ async function recall(query, ctx, options = {}) {
19632
19961
  memories = selection.out;
19633
19962
  }
19634
19963
  hitLimit = selection.cut;
19635
- fuseMs = nowMs2() - fuseStart;
19964
+ fuseMs = nowMs() - fuseStart;
19636
19965
  emitDiagnostics(byId.size, memories);
19637
19966
  return {
19638
19967
  memories,
@@ -29635,28 +29964,6 @@ async function cleanupConversationSummary(database, conversationId) {
29635
29964
  }
29636
29965
  }
29637
29966
 
29638
- // src/lib/db/chat/embeddingCache.ts
29639
- var MaskScopedEmbeddingCache = class extends Map {
29640
- constructor(inner, masked) {
29641
- super();
29642
- this.inner = inner;
29643
- this.masked = masked;
29644
- }
29645
- key(text14) {
29646
- return `${this.masked ? "m" : "r"}:${text14}`;
29647
- }
29648
- get(text14) {
29649
- return this.inner.get(this.key(text14));
29650
- }
29651
- set(text14, value) {
29652
- this.inner.set(this.key(text14), value);
29653
- return this;
29654
- }
29655
- };
29656
- function maskScopedEmbeddingCache(cache, masked) {
29657
- return new MaskScopedEmbeddingCache(cache, masked);
29658
- }
29659
-
29660
29967
  // src/lib/tools/selection/activeToolSets.ts
29661
29968
  function mergeActiveToolSets(current, derived) {
29662
29969
  if (derived.length === 0) return current;
@@ -30185,11 +30492,12 @@ async function storedToLlmapiMessage(stored2, encryptionKey, resolveMediaByIds)
30185
30492
  textContent = textContent.replace(/\n{3,}/g, "\n\n").trim();
30186
30493
  const content = [{ type: "text", text: textContent }, ...imageParts];
30187
30494
  const messages = [];
30188
- if (stored2.role === "assistant" && stored2.toolCallEvents?.length) {
30495
+ const toolCallEvents = Array.isArray(stored2.toolCallEvents) ? stored2.toolCallEvents.filter((event) => !hasEncryptedToolPayload(event)) : [];
30496
+ if (stored2.role === "assistant" && toolCallEvents.length) {
30189
30497
  messages.push({
30190
30498
  role: stored2.role,
30191
30499
  content: void 0,
30192
- tool_calls: stored2.toolCallEvents.map((event) => ({
30500
+ tool_calls: toolCallEvents.map((event) => ({
30193
30501
  id: event.id,
30194
30502
  type: "function",
30195
30503
  function: {
@@ -30198,7 +30506,7 @@ async function storedToLlmapiMessage(stored2, encryptionKey, resolveMediaByIds)
30198
30506
  }
30199
30507
  }))
30200
30508
  });
30201
- for (const event of stored2.toolCallEvents) {
30509
+ for (const event of toolCallEvents) {
30202
30510
  if (event.id && event.output !== void 0 && event.output !== null) {
30203
30511
  messages.push({
30204
30512
  role: "tool",
@@ -35190,6 +35498,7 @@ export {
35190
35498
  encryptDataBatch,
35191
35499
  encryptDataBytes,
35192
35500
  encryptDataWithKey,
35501
+ encryptToolCallEventsOp,
35193
35502
  ensureDefaultFoldersOp,
35194
35503
  expandToolSetsAdditive,
35195
35504
  exportElementToPdf,