@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.
@@ -1314,6 +1314,35 @@ function decodeChunkVector(vector, onMalformed) {
1314
1314
  }
1315
1315
  }
1316
1316
 
1317
+ // src/lib/scheduling.ts
1318
+ function nowMs() {
1319
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
1320
+ }
1321
+ function yieldToEventLoop() {
1322
+ const scheduler = globalThis.scheduler;
1323
+ if (typeof scheduler?.yield === "function") return scheduler.yield();
1324
+ if (typeof MessageChannel === "function") {
1325
+ return new Promise((resolve) => {
1326
+ const channel = new MessageChannel();
1327
+ channel.port1.onmessage = () => {
1328
+ channel.port1.close();
1329
+ channel.port2.close();
1330
+ resolve();
1331
+ };
1332
+ channel.port2.postMessage(null);
1333
+ });
1334
+ }
1335
+ return new Promise((resolve) => setTimeout(resolve, 0));
1336
+ }
1337
+ function createTimeSliceYielder(budgetMs) {
1338
+ let sliceStart = nowMs();
1339
+ return async () => {
1340
+ if (nowMs() - sliceStart < budgetMs) return;
1341
+ await yieldToEventLoop();
1342
+ sliceStart = nowMs();
1343
+ };
1344
+ }
1345
+
1317
1346
  // src/lib/db/encryption-utils.ts
1318
1347
  var ENCRYPTION_PREFIX = "enc:v3:";
1319
1348
  function isEncrypted(value) {
@@ -1322,6 +1351,11 @@ function isEncrypted(value) {
1322
1351
  const payload = value.slice(prefix.length);
1323
1352
  return payload.length >= 56 && /^[0-9a-f]+$/i.test(payload);
1324
1353
  }
1354
+ function hasEncryptedToolPayload(event) {
1355
+ return [event.arguments, event.output].some(
1356
+ (value) => typeof value === "string" && isEncrypted(value)
1357
+ );
1358
+ }
1325
1359
  function detectEncryptionVersion(value) {
1326
1360
  if (value.startsWith("enc:v3:")) {
1327
1361
  return { version: "v3", encryptedData: value.slice("enc:v3:".length) };
@@ -1457,6 +1491,12 @@ async function encryptMessageFields(message, address, signMessage, embeddedWalle
1457
1491
  const encryptedChunks = msg.chunks ? await encryptJsonField(msg.chunks, address, signMessage, embeddedWalletSigner, true) : void 0;
1458
1492
  const encryptedSources = msg.sources ? await encryptJsonField(msg.sources, address, signMessage, embeddedWalletSigner, true) : void 0;
1459
1493
  const encryptedThoughtProcess = msg.thoughtProcess ? await encryptJsonField(msg.thoughtProcess, address, signMessage, embeddedWalletSigner, true) : void 0;
1494
+ const encryptedToolCallEvents = Array.isArray(msg.toolCallEvents) ? await encryptToolCallEvents(
1495
+ msg.toolCallEvents,
1496
+ address,
1497
+ signMessage,
1498
+ embeddedWalletSigner
1499
+ ) : void 0;
1460
1500
  return {
1461
1501
  ...message,
1462
1502
  ...encryptedContent !== void 0 && { content: encryptedContent },
@@ -1464,13 +1504,31 @@ async function encryptMessageFields(message, address, signMessage, embeddedWalle
1464
1504
  ...encryptedVector !== void 0 && { vector: encryptedVector },
1465
1505
  ...encryptedChunks !== void 0 && { chunks: encryptedChunks },
1466
1506
  ...encryptedSources !== void 0 && { sources: encryptedSources },
1467
- ...encryptedThoughtProcess !== void 0 && { thoughtProcess: encryptedThoughtProcess }
1507
+ ...encryptedThoughtProcess !== void 0 && { thoughtProcess: encryptedThoughtProcess },
1508
+ ...encryptedToolCallEvents !== void 0 && { toolCallEvents: encryptedToolCallEvents }
1468
1509
  };
1469
1510
  } catch (error) {
1470
1511
  getLogger().warn("Failed to encrypt message fields:", error);
1471
1512
  throw error;
1472
1513
  }
1473
1514
  }
1515
+ function mapToolCallPayloads(events, transform) {
1516
+ return Promise.all(
1517
+ events.map(async (event) => {
1518
+ if (!event || typeof event !== "object") return event;
1519
+ const mapped = { ...event };
1520
+ if (typeof event.arguments === "string") mapped.arguments = await transform(event.arguments);
1521
+ if (typeof event.output === "string") mapped.output = await transform(event.output);
1522
+ return mapped;
1523
+ })
1524
+ );
1525
+ }
1526
+ function encryptToolCallEvents(events, address, signMessage, embeddedWalletSigner) {
1527
+ return mapToolCallPayloads(
1528
+ events,
1529
+ (value) => encryptField(value, address, signMessage, embeddedWalletSigner, true)
1530
+ );
1531
+ }
1474
1532
  async function decryptMaybeJsonFieldDetailed(value, address) {
1475
1533
  if (value === void 0) return { value: void 0, status: "plaintext" };
1476
1534
  if (value === null) return { value, status: "plaintext" };
@@ -1556,6 +1614,10 @@ async function assembleDecryptedMessage(message, address) {
1556
1614
  message.thoughtProcess,
1557
1615
  address
1558
1616
  );
1617
+ const toolCallEventsResult = await decryptMaybeJsonFieldDetailed(
1618
+ message.toolCallEvents,
1619
+ address
1620
+ );
1559
1621
  let decryptionStatus;
1560
1622
  let refreshProbe;
1561
1623
  const consider = (status, probe) => {
@@ -1574,6 +1636,14 @@ async function assembleDecryptedMessage(message, address) {
1574
1636
  if (thoughtProcessResult.probe) {
1575
1637
  consider(thoughtProcessResult.status, thoughtProcessResult.probe);
1576
1638
  }
1639
+ if (toolCallEventsResult.probe) {
1640
+ consider(toolCallEventsResult.status, toolCallEventsResult.probe);
1641
+ }
1642
+ const toolCallEvents = Array.isArray(toolCallEventsResult.value) ? await mapToolCallPayloads(toolCallEventsResult.value, async (value) => {
1643
+ const detailed = await decryptFieldDetailed(value, address);
1644
+ consider(detailed.status, isEncrypted(value) ? value : void 0);
1645
+ return detailed.value;
1646
+ }) : void 0;
1577
1647
  const statusForConsumer = decryptionStatus === "ok" || decryptionStatus === "plaintext" || !decryptionStatus ? void 0 : decryptionStatus;
1578
1648
  const result = {
1579
1649
  ...message,
@@ -1582,7 +1652,8 @@ async function assembleDecryptedMessage(message, address) {
1582
1652
  vector: vectorResult.value,
1583
1653
  chunks: chunksResult.value,
1584
1654
  sources: sourcesResult.value,
1585
- thoughtProcess: thoughtProcessResult.value
1655
+ thoughtProcess: thoughtProcessResult.value,
1656
+ toolCallEvents
1586
1657
  };
1587
1658
  if (statusForConsumer) {
1588
1659
  result.decryptionStatus = statusForConsumer;
@@ -2378,7 +2449,7 @@ async function updateMessageChunksOp(ctx, uniqueId, chunks3, embeddingModel) {
2378
2449
  });
2379
2450
  return messageToStored(message, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner);
2380
2451
  }
2381
- var STRIP_CHUNK_TEXT_PAGE_SIZE = 100;
2452
+ var RAW_REPAIR_PAGE_SIZE = 100;
2382
2453
  function chunksWithoutText(raw) {
2383
2454
  if (typeof raw !== "string") return null;
2384
2455
  let parsed;
@@ -2401,8 +2472,8 @@ function chunksWithoutText(raw) {
2401
2472
  async function stripLegacyChunkTextOp(ctx) {
2402
2473
  const candidateIds = await ctx.messagesCollection.query(Q.where("chunks", Q.like(`%${Q.sanitizeLikeString('"text":')}%`))).fetchIds();
2403
2474
  let stripped = 0;
2404
- for (let start = 0; start < candidateIds.length; start += STRIP_CHUNK_TEXT_PAGE_SIZE) {
2405
- const pageIds = candidateIds.slice(start, start + STRIP_CHUNK_TEXT_PAGE_SIZE);
2475
+ for (let start = 0; start < candidateIds.length; start += RAW_REPAIR_PAGE_SIZE) {
2476
+ const pageIds = candidateIds.slice(start, start + RAW_REPAIR_PAGE_SIZE);
2406
2477
  const rows = await ctx.messagesCollection.query(Q.where("id", Q.oneOf(pageIds))).unsafeFetchRaw();
2407
2478
  const changedIds = rows.filter((row) => chunksWithoutText(row.chunks) !== null).map((row) => String(row.id));
2408
2479
  if (changedIds.length === 0) continue;
@@ -2426,6 +2497,88 @@ async function stripLegacyChunkTextOp(ctx) {
2426
2497
  }
2427
2498
  return stripped;
2428
2499
  }
2500
+ async function encryptedToolCallEventsColumn(raw, address, signMessage, embeddedWalletSigner) {
2501
+ if (typeof raw !== "string") return null;
2502
+ let parsed;
2503
+ try {
2504
+ parsed = JSON.parse(raw);
2505
+ } catch {
2506
+ return null;
2507
+ }
2508
+ if (!Array.isArray(parsed)) return null;
2509
+ const column = JSON.stringify(
2510
+ await encryptToolCallEvents(
2511
+ parsed,
2512
+ address,
2513
+ signMessage,
2514
+ embeddedWalletSigner
2515
+ )
2516
+ );
2517
+ return column === JSON.stringify(parsed) ? null : column;
2518
+ }
2519
+ async function encryptToolCallEventsOp(ctx) {
2520
+ const { walletAddress: address, signMessage, embeddedWalletSigner } = ctx;
2521
+ if (!address || !signMessage) return 0;
2522
+ if (!await requestEncryptionKey(address, signMessage, embeddedWalletSigner)) return 0;
2523
+ if (!hasEncryptionKey(address)) return 0;
2524
+ if (!await savedKeyOpensExistingData(ctx, address, signMessage, embeddedWalletSigner)) {
2525
+ return 0;
2526
+ }
2527
+ const candidateIds = await ctx.messagesCollection.query(
2528
+ Q.where("tool_call_events", Q.notEq(null)),
2529
+ Q.where("tool_call_events", Q.notIn(["", "[]"]))
2530
+ ).fetchIds();
2531
+ let encrypted = 0;
2532
+ for (let start = 0; start < candidateIds.length; start += RAW_REPAIR_PAGE_SIZE) {
2533
+ const pageIds = candidateIds.slice(start, start + RAW_REPAIR_PAGE_SIZE);
2534
+ const rows = await ctx.messagesCollection.query(Q.where("id", Q.oneOf(pageIds))).unsafeFetchRaw();
2535
+ const repairs = /* @__PURE__ */ new Map();
2536
+ for (const row of rows) {
2537
+ const column = await encryptedToolCallEventsColumn(
2538
+ row.tool_call_events,
2539
+ address,
2540
+ signMessage,
2541
+ embeddedWalletSigner
2542
+ );
2543
+ if (column !== null) repairs.set(String(row.id), { raw: row.tool_call_events, column });
2544
+ }
2545
+ if (repairs.size === 0) continue;
2546
+ await ctx.database.write(async () => {
2547
+ const messages = (await ctx.messagesCollection.query(Q.where("id", Q.oneOf([...repairs.keys()]))).fetch()).filter(
2548
+ (message) => message._raw.tool_call_events === repairs.get(message.id)?.raw
2549
+ );
2550
+ if (messages.length === 0) return;
2551
+ const conversationIds = [...new Set(messages.map((message) => message.conversationId))];
2552
+ const conversations = await ctx.conversationsCollection.query(Q.where("conversation_id", Q.oneOf(conversationIds))).fetch();
2553
+ await ctx.database.batch(
2554
+ ...messages.map(
2555
+ (message) => message.prepareUpdate((msg) => {
2556
+ msg._setRaw("tool_call_events", repairs.get(message.id).column);
2557
+ })
2558
+ ),
2559
+ ...conversations.map((conversation) => conversation.prepareUpdate(() => {
2560
+ }))
2561
+ );
2562
+ encrypted += messages.length;
2563
+ });
2564
+ }
2565
+ return encrypted;
2566
+ }
2567
+ async function savedKeyOpensExistingData(ctx, address, signMessage, embeddedWalletSigner) {
2568
+ const v3Probe = await findEncryptedContent(ctx, "enc:v3:%");
2569
+ if (v3Probe) {
2570
+ const { status } = await decryptFieldDetailed(v3Probe, address);
2571
+ if (status !== "auth_mismatch" && status !== "key_missing") return true;
2572
+ return refreshEncryptionKeyIfMatches(address, v3Probe, signMessage, embeddedWalletSigner);
2573
+ }
2574
+ const v2Probe = await findEncryptedContent(ctx, "enc:v2:%");
2575
+ if (!v2Probe) return true;
2576
+ return refreshEncryptionKeyIfMatches(address, v2Probe, signMessage, embeddedWalletSigner);
2577
+ }
2578
+ async function findEncryptedContent(ctx, pattern) {
2579
+ const [row] = await ctx.messagesCollection.query(Q.where("content", Q.like(pattern)), Q.take(1)).unsafeFetchRaw();
2580
+ return row ? String(row.content) : null;
2581
+ }
2429
2582
  async function updateMessageErrorOp(ctx, uniqueId, error) {
2430
2583
  let message;
2431
2584
  try {
@@ -2523,6 +2676,95 @@ async function readRawJsonField(raw, column, walletAddress) {
2523
2676
  return void 0;
2524
2677
  }
2525
2678
  }
2679
+ var CHUNK_VECTOR_BATCH_SIZE = 50;
2680
+ var CHUNK_SCAN_YIELD_BUDGET_MS = 12;
2681
+ var hasEmbeddingSql = `(coalesce("chunks", '') != '' or coalesce("vector", '') != '')`;
2682
+ function toChunkCandidateKey(raw) {
2683
+ return {
2684
+ id: raw.id,
2685
+ conversationId: String(raw.conversation_id),
2686
+ version: Number(raw.updated_at) || 0,
2687
+ embeddingModel: raw.embedding_model ?? null
2688
+ };
2689
+ }
2690
+ async function fetchRowsByIds(ctx, ids) {
2691
+ if (ids.length === 0) return [];
2692
+ return await ctx.messagesCollection.query(Q.where("id", Q.oneOf(ids))).unsafeFetchRaw();
2693
+ }
2694
+ async function projectedChunkRowSource(ctx, filter) {
2695
+ const clauses = [
2696
+ `"history"."_status" is not 'deleted'`,
2697
+ hasEmbeddingSql,
2698
+ `exists (select 1 from "conversations" where "conversations"."conversation_id" = "history"."conversation_id" and "conversations"."is_deleted" = 0 and "conversations"."_status" is not 'deleted')`
2699
+ ];
2700
+ const args = [];
2701
+ if (filter.conversationId !== void 0) {
2702
+ clauses.push(`"history"."conversation_id" = ?`);
2703
+ args.push(filter.conversationId);
2704
+ }
2705
+ if (filter.excludeConversationId !== void 0) {
2706
+ clauses.push(`"history"."conversation_id" != ?`);
2707
+ args.push(filter.excludeConversationId);
2708
+ }
2709
+ const keyRows = await ctx.messagesCollection.query(
2710
+ Q.unsafeSqlQuery(
2711
+ `select "id", "conversation_id", "updated_at", "embedding_model" from "history" where ${clauses.join(" and ")}`,
2712
+ args
2713
+ )
2714
+ ).unsafeFetchRaw();
2715
+ return {
2716
+ keys: keyRows.map(toChunkCandidateKey),
2717
+ loadVectorRows: async (ids) => {
2718
+ if (ids.length === 0) return [];
2719
+ return await ctx.messagesCollection.query(
2720
+ Q.unsafeSqlQuery(
2721
+ `select "id", "updated_at", "chunks", "vector" from "history" where "_status" is not 'deleted' and "id" in (${ids.map(() => "?").join(",")})`,
2722
+ ids
2723
+ )
2724
+ ).unsafeFetchRaw();
2725
+ },
2726
+ loadFullRows: (ids) => fetchRowsByIds(ctx, ids)
2727
+ };
2728
+ }
2729
+ async function fullScanChunkRowSource(ctx, filter) {
2730
+ const activeConversations = await ctx.conversationsCollection.query(Q.where("is_deleted", false)).fetch();
2731
+ const activeConversationIds = new Set(
2732
+ activeConversations.map((c) => String(c._getRaw("conversation_id")))
2733
+ );
2734
+ const queryConditions = filter.conversationId !== void 0 ? [Q.where("conversation_id", filter.conversationId)] : [];
2735
+ const rows = await ctx.messagesCollection.query(...queryConditions).unsafeFetchRaw();
2736
+ const rowsById = /* @__PURE__ */ new Map();
2737
+ for (const row of rows) {
2738
+ const conversationId = String(row.conversation_id);
2739
+ if (!activeConversationIds.has(conversationId)) continue;
2740
+ if (filter.excludeConversationId !== void 0 && conversationId === filter.excludeConversationId) {
2741
+ continue;
2742
+ }
2743
+ if (!row.chunks && !row.vector) continue;
2744
+ rowsById.set(row.id, row);
2745
+ }
2746
+ const pick = (ids) => Promise.resolve(
2747
+ ids.flatMap((id) => {
2748
+ const row = rowsById.get(id);
2749
+ return row ? [row] : [];
2750
+ })
2751
+ );
2752
+ return {
2753
+ keys: Array.from(rowsById.values(), toChunkCandidateKey),
2754
+ loadVectorRows: pick,
2755
+ loadFullRows: pick
2756
+ };
2757
+ }
2758
+ async function chunkRowSource(ctx, filter) {
2759
+ try {
2760
+ return await projectedChunkRowSource(ctx, filter);
2761
+ } catch (err) {
2762
+ getLogger().debug(
2763
+ "searchChunksOp: projected SQL unavailable, using full-load fallback: " + (err instanceof Error ? err.message : String(err))
2764
+ );
2765
+ return fullScanChunkRowSource(ctx, filter);
2766
+ }
2767
+ }
2526
2768
  async function searchChunksOp(ctx, queryVector, options) {
2527
2769
  const {
2528
2770
  limit = 10,
@@ -2530,83 +2772,111 @@ async function searchChunksOp(ctx, queryVector, options) {
2530
2772
  conversationId,
2531
2773
  excludeConversationId,
2532
2774
  embeddingModel,
2533
- chunkCache
2775
+ chunkCache,
2776
+ onStats
2534
2777
  } = options || {};
2535
- const activeConversations = await ctx.conversationsCollection.query(Q.where("is_deleted", false)).fetch();
2536
- const activeConversationIds = new Set(
2537
- activeConversations.map((c) => String(c._getRaw("conversation_id")))
2538
- );
2539
- const queryConditions = conversationId ? [Q.where("conversation_id", conversationId)] : [];
2540
- const messages = await ctx.messagesCollection.query(...queryConditions).unsafeFetchRaw();
2778
+ const source = await chunkRowSource(ctx, { conversationId, excludeConversationId });
2779
+ const yieldIfDue = createTimeSliceYielder(CHUNK_SCAN_YIELD_BUDGET_MS);
2541
2780
  const candidates = [];
2542
2781
  let staleSkipped = 0;
2543
2782
  let unreadableVectors = 0;
2544
- for (const message of messages) {
2545
- const msgConvId = String(message.conversation_id);
2546
- if (!activeConversationIds.has(msgConvId)) continue;
2547
- if (excludeConversationId !== void 0 && msgConvId === excludeConversationId) continue;
2548
- if (embeddingModel) {
2549
- const storedModel = message.embedding_model;
2550
- if (storedModel !== null && storedModel !== void 0 && storedModel !== embeddingModel) {
2551
- staleSkipped++;
2552
- continue;
2553
- }
2783
+ let cacheHits = 0;
2784
+ const missIds = [];
2785
+ const orderById = /* @__PURE__ */ new Map();
2786
+ for (const [order, key] of source.keys.entries()) {
2787
+ await yieldIfDue();
2788
+ orderById.set(key.id, order);
2789
+ if (embeddingModel && key.embeddingModel !== null && key.embeddingModel !== embeddingModel) {
2790
+ staleSkipped++;
2791
+ continue;
2554
2792
  }
2555
- const version = Number(message.updated_at) || 0;
2556
- const cached = chunkCache?.get(message.id);
2557
- if (cached && cached.version === version) {
2558
- if (cached.chunks.length > 0) {
2559
- for (let ci = 0; ci < cached.chunks.length; ci++) {
2560
- const vec = cached.chunks[ci];
2561
- if (vec.length === 0) continue;
2562
- const similarity = cosineSimilarity(queryVector, vec);
2563
- if (similarity >= minSimilarity) {
2564
- candidates.push({
2565
- message,
2566
- similarity,
2567
- chunkTextSource: { kind: "chunk-cached", chunkIndex: ci, version }
2568
- });
2569
- }
2570
- }
2571
- } else if (cached.fallback) {
2572
- const similarity = cosineSimilarity(queryVector, cached.fallback);
2573
- if (similarity >= minSimilarity) {
2574
- candidates.push({ message, similarity, chunkTextSource: { kind: "message" } });
2575
- }
2576
- }
2793
+ const cached = chunkCache?.get(key.id);
2794
+ if (!cached || cached.version !== key.version) {
2795
+ missIds.push(key.id);
2577
2796
  continue;
2578
2797
  }
2579
- const chunks3 = await readRawJsonField(message, "chunks", ctx.walletAddress);
2580
- if (chunks3 && chunks3.length > 0) {
2581
- const vectors = chunks3.map(
2582
- (chunk) => decodeChunkVector(chunk.vector, () => {
2583
- unreadableVectors++;
2584
- })
2585
- );
2586
- chunkCache?.set(message.id, { version, chunks: vectors });
2587
- for (let ci = 0; ci < chunks3.length; ci++) {
2588
- const vec = vectors[ci];
2798
+ cacheHits++;
2799
+ if (cached.chunks.length > 0) {
2800
+ for (let ci = 0; ci < cached.chunks.length; ci++) {
2801
+ const vec = cached.chunks[ci];
2589
2802
  if (vec.length === 0) continue;
2590
2803
  const similarity = cosineSimilarity(queryVector, vec);
2591
2804
  if (similarity >= minSimilarity) {
2592
2805
  candidates.push({
2593
- message,
2806
+ id: key.id,
2807
+ version: key.version,
2808
+ order,
2594
2809
  similarity,
2595
- chunkTextSource: { kind: "chunk", chunk: chunks3[ci], siblings: chunks3 }
2810
+ chunkTextSource: { kind: "chunk-cached", chunkIndex: ci, version: key.version }
2596
2811
  });
2597
2812
  }
2598
2813
  }
2599
- } else {
2600
- const messageVector = await readRawJsonField(message, "vector", ctx.walletAddress);
2601
- if (!messageVector || messageVector.length === 0) continue;
2602
- const fallback = Float32Array.from(messageVector);
2603
- chunkCache?.set(message.id, { version, chunks: [], fallback });
2604
- const similarity = cosineSimilarity(queryVector, fallback);
2814
+ } else if (cached.fallback) {
2815
+ const similarity = cosineSimilarity(queryVector, cached.fallback);
2605
2816
  if (similarity >= minSimilarity) {
2606
- candidates.push({ message, similarity, chunkTextSource: { kind: "message" } });
2817
+ candidates.push({
2818
+ id: key.id,
2819
+ version: key.version,
2820
+ order,
2821
+ similarity,
2822
+ chunkTextSource: { kind: "message" }
2823
+ });
2607
2824
  }
2608
2825
  }
2609
2826
  }
2827
+ let rowsFetched = 0;
2828
+ for (let offset = 0; offset < missIds.length; offset += CHUNK_VECTOR_BATCH_SIZE) {
2829
+ const rows = await source.loadVectorRows(
2830
+ missIds.slice(offset, offset + CHUNK_VECTOR_BATCH_SIZE)
2831
+ );
2832
+ rowsFetched += rows.length;
2833
+ for (const row of rows) {
2834
+ await yieldIfDue();
2835
+ const id = row.id;
2836
+ const version = Number(row.updated_at) || 0;
2837
+ const order = orderById.get(id) ?? Number.MAX_SAFE_INTEGER;
2838
+ const chunks3 = await readRawJsonField(row, "chunks", ctx.walletAddress);
2839
+ if (chunks3 && chunks3.length > 0) {
2840
+ const vectors = chunks3.map(
2841
+ (chunk) => decodeChunkVector(chunk.vector, () => {
2842
+ unreadableVectors++;
2843
+ })
2844
+ );
2845
+ chunkCache?.set(id, { version, chunks: vectors });
2846
+ for (let ci = 0; ci < chunks3.length; ci++) {
2847
+ const vec = vectors[ci];
2848
+ if (vec.length === 0) continue;
2849
+ const similarity = cosineSimilarity(queryVector, vec);
2850
+ if (similarity >= minSimilarity) {
2851
+ candidates.push({
2852
+ id,
2853
+ version,
2854
+ order,
2855
+ similarity,
2856
+ chunkTextSource: { kind: "chunk", chunk: chunks3[ci], siblings: chunks3 }
2857
+ });
2858
+ }
2859
+ }
2860
+ } else {
2861
+ const messageVector = await readRawJsonField(row, "vector", ctx.walletAddress);
2862
+ if (messageVector && messageVector.length > 0) {
2863
+ const fallback = Float32Array.from(messageVector);
2864
+ chunkCache?.set(id, { version, chunks: [], fallback });
2865
+ const similarity = cosineSimilarity(queryVector, fallback);
2866
+ if (similarity >= minSimilarity) {
2867
+ candidates.push({
2868
+ id,
2869
+ version,
2870
+ order,
2871
+ similarity,
2872
+ chunkTextSource: { kind: "message" }
2873
+ });
2874
+ }
2875
+ }
2876
+ }
2877
+ }
2878
+ }
2879
+ onStats?.({ rowsScanned: source.keys.length, rowsFetched, cacheHits });
2610
2880
  if (staleSkipped > 0) {
2611
2881
  getLogger().warn(
2612
2882
  `searchChunksOp: skipped ${staleSkipped} messages whose embedding model differs from the current model (${embeddingModel}) \u2014 re-embed via chunkAndEmbedAllMessages`
@@ -2617,12 +2887,17 @@ async function searchChunksOp(ctx, queryVector, options) {
2617
2887
  `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`
2618
2888
  );
2619
2889
  }
2620
- const topK = candidates.sort((a, b) => b.similarity - a.similarity).slice(0, limit);
2621
- const uniqueMessages = /* @__PURE__ */ new Map();
2622
- for (const c of topK) uniqueMessages.set(c.message.id, c.message);
2890
+ const topK = candidates.sort((a, b) => b.similarity - a.similarity || a.order - b.order).slice(0, limit);
2891
+ const topIds = Array.from(new Set(topK.map((c) => c.id)));
2892
+ const fullRows = await source.loadFullRows(topIds);
2893
+ const rowById = new Map(fullRows.map((row) => [row.id, row]));
2894
+ const liveTopK = topK.filter((c) => {
2895
+ const row = rowById.get(c.id);
2896
+ return row !== void 0 && (Number(row.updated_at) || 0) === c.version;
2897
+ });
2623
2898
  const storedById = new Map(
2624
2899
  await Promise.all(
2625
- Array.from(uniqueMessages, async ([id, m]) => {
2900
+ Array.from(rowById, async ([id, m]) => {
2626
2901
  const stored2 = await messageRawToStored(
2627
2902
  m,
2628
2903
  ctx.walletAddress,
@@ -2635,24 +2910,24 @@ async function searchChunksOp(ctx, queryVector, options) {
2635
2910
  );
2636
2911
  const chunkStateByMsgId = /* @__PURE__ */ new Map();
2637
2912
  const needsChunkText = /* @__PURE__ */ new Set();
2638
- for (const c of topK) {
2639
- if (c.chunkTextSource.kind === "chunk-cached") needsChunkText.add(c.message.id);
2913
+ for (const c of liveTopK) {
2914
+ if (c.chunkTextSource.kind === "chunk-cached") needsChunkText.add(c.id);
2640
2915
  }
2641
2916
  await Promise.all(
2642
2917
  Array.from(needsChunkText, async (id) => {
2643
- const m = uniqueMessages.get(id);
2918
+ const m = rowById.get(id);
2644
2919
  const parsed = await readRawJsonField(m, "chunks", ctx.walletAddress);
2645
2920
  const version = Number(m.updated_at) || 0;
2646
2921
  if (parsed) chunkStateByMsgId.set(id, { version, chunks: parsed });
2647
2922
  })
2648
2923
  );
2649
- return topK.map(({ message, similarity, chunkTextSource }) => {
2650
- const stored2 = { ...storedById.get(message.id) };
2924
+ return liveTopK.map(({ id, similarity, chunkTextSource }) => {
2925
+ const stored2 = { ...storedById.get(id) };
2651
2926
  let chunkText2;
2652
2927
  if (chunkTextSource.kind === "chunk") {
2653
2928
  chunkText2 = resolveChunkText(stored2.content, chunkTextSource.chunk, chunkTextSource.siblings);
2654
2929
  } else if (chunkTextSource.kind === "chunk-cached") {
2655
- const state = chunkStateByMsgId.get(message.id);
2930
+ const state = chunkStateByMsgId.get(id);
2656
2931
  const chunk = state && state.version === chunkTextSource.version ? state.chunks[chunkTextSource.chunkIndex] : void 0;
2657
2932
  chunkText2 = resolveChunkText(stored2.content, chunk, state?.chunks);
2658
2933
  } else {
@@ -3731,13 +4006,35 @@ async function generateEmbedding(text14, options) {
3731
4006
  if (total === void 0) return embedOne(text14, options, void 0);
3732
4007
  return withDeadline((signal) => embedOne(text14, options, signal), total, "embedding");
3733
4008
  }
4009
+ var inFlightByCache = /* @__PURE__ */ new WeakMap();
3734
4010
  async function embedOne(text14, options, outer) {
3735
- const { baseUrl = BASE_URL, model, cache } = options;
3736
- const timeoutMs = options.timeoutMs ?? DEFAULT_EMBEDDING_REQUEST_TIMEOUT_MS;
3737
- if (cache) {
3738
- const cached = cache.get(text14);
3739
- if (cached) return Array.from(cached);
4011
+ const { cache } = options;
4012
+ if (!cache) return Array.from(await requestEmbedding(text14, options, outer));
4013
+ const cached = cache.get(text14);
4014
+ if (cached) return Array.from(cached);
4015
+ const inFlight = inFlightByCache.get(cache) ?? /* @__PURE__ */ new Map();
4016
+ inFlightByCache.set(cache, inFlight);
4017
+ const pending = inFlight.get(text14);
4018
+ if (pending) {
4019
+ try {
4020
+ return Array.from(await pending);
4021
+ } catch (err) {
4022
+ if (outer?.aborted) throw err;
4023
+ }
4024
+ }
4025
+ const request = requestEmbedding(text14, options, outer);
4026
+ inFlight.set(text14, request);
4027
+ try {
4028
+ const embedding2 = await request;
4029
+ cache.set(text14, embedding2);
4030
+ return Array.from(embedding2);
4031
+ } finally {
4032
+ if (inFlight.get(text14) === request) inFlight.delete(text14);
3740
4033
  }
4034
+ }
4035
+ async function requestEmbedding(text14, options, outer) {
4036
+ const { baseUrl = BASE_URL, model } = options;
4037
+ const timeoutMs = options.timeoutMs ?? DEFAULT_EMBEDDING_REQUEST_TIMEOUT_MS;
3741
4038
  const headers = {
3742
4039
  ...await resolveAuthHeaders(options, outer),
3743
4040
  ...taskTypeHeader(options.taskType)
@@ -3772,11 +4069,7 @@ async function embedOne(text14, options, outer) {
3772
4069
  totalTokens: response.data.usage.total_tokens ?? 0
3773
4070
  });
3774
4071
  }
3775
- const f32Embedding = Float32Array.from(embedding2);
3776
- if (cache) {
3777
- cache.set(text14, f32Embedding);
3778
- }
3779
- return Array.from(f32Embedding);
4072
+ return Float32Array.from(embedding2);
3780
4073
  }
3781
4074
  var DEFAULT_EMBEDDING_BATCH_SIZE = 100;
3782
4075
  var DEFAULT_EMBEDDING_BATCH_CONCURRENCY = 3;
@@ -4174,7 +4467,8 @@ function extractReservationReceipts(toolCallEvents) {
4174
4467
  for (const event of toolCallEvents ?? []) {
4175
4468
  const action = ACTIONS.find((a) => event.name?.endsWith(a.tool));
4176
4469
  const output = event.output ?? "";
4177
- if (!action || !output || NOT_AN_ATTEMPT.test(output)) continue;
4470
+ if (!action || !output || hasEncryptedToolPayload(event) || NOT_AN_ATTEMPT.test(output))
4471
+ continue;
4178
4472
  const kept = latest.get(action.kind)?.status;
4179
4473
  if (kept === "made") continue;
4180
4474
  const receipt = toReceipt(action, event.arguments ?? "", output);
@@ -9257,8 +9551,12 @@ var MaskScopedEmbeddingCache = class extends Map {
9257
9551
  return this;
9258
9552
  }
9259
9553
  };
9554
+ var scopedViews = /* @__PURE__ */ new WeakMap();
9260
9555
  function maskScopedEmbeddingCache(cache, masked) {
9261
- return new MaskScopedEmbeddingCache(cache, masked);
9556
+ const views = scopedViews.get(cache) ?? {};
9557
+ scopedViews.set(cache, views);
9558
+ const slot = masked ? "masked" : "raw";
9559
+ return views[slot] ?? (views[slot] = new MaskScopedEmbeddingCache(cache, masked));
9262
9560
  }
9263
9561
 
9264
9562
  // src/lib/db/conversationMemory/models.ts
@@ -13602,9 +13900,9 @@ function recencyMultiplier(updatedAt, opts = {}) {
13602
13900
  const updatedMs = updatedAt.getTime();
13603
13901
  if (!Number.isFinite(updatedMs)) return noDate;
13604
13902
  const now = opts.now ?? /* @__PURE__ */ new Date();
13605
- const nowMs3 = now.getTime();
13606
- if (!Number.isFinite(nowMs3)) return noDate;
13607
- const ageDays2 = Math.max(0, (nowMs3 - updatedMs) / DAY_MS2);
13903
+ const nowMs2 = now.getTime();
13904
+ if (!Number.isFinite(nowMs2)) return noDate;
13905
+ const ageDays2 = Math.max(0, (nowMs2 - updatedMs) / DAY_MS2);
13608
13906
  const perYearDecay = opts.perYearDecay ?? DEFAULT_PER_YEAR_DECAY;
13609
13907
  const floor = opts.floor ?? DEFAULT_FLOOR;
13610
13908
  const decay = perYearDecay * (ageDays2 / 365);
@@ -14113,9 +14411,6 @@ function createVaultEmbeddingCache(maxSize = DEFAULT_VAULT_CACHE_SIZE) {
14113
14411
 
14114
14412
  // src/lib/memoryVault/searchTool.ts
14115
14413
  var DEFAULT_RERANK_TOP_N = 5;
14116
- function nowMs() {
14117
- return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
14118
- }
14119
14414
  var warnedProgrammaticDecomposeIgnored = false;
14120
14415
  function normalizeEventTimeKind(kind) {
14121
14416
  return kind === "point" || kind === "range" || kind === "ongoing" ? kind : null;
@@ -17213,9 +17508,6 @@ function scoreEventTimeOverlap(memoryStart, memoryEnd, memoryKind, window2) {
17213
17508
  var DEFAULT_LIMIT = 8;
17214
17509
  var DEFAULT_BUDGET = "low";
17215
17510
  var DEFAULT_FACT_MIN_SCORE = 0.1;
17216
- function nowMs2() {
17217
- return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
17218
- }
17219
17511
  var DEFAULT_CHUNK_MIN_SCORE = 0.5;
17220
17512
  var DEFAULT_QUERY_EMBED_TOTAL_TIMEOUT_MS = 8e3;
17221
17513
  function flagsForBudget(budget) {
@@ -17253,12 +17545,19 @@ async function recall(query, ctx, options = {}) {
17253
17545
  const subQueries = normalizedFacets.length >= 2 ? normalizedFacets : void 0;
17254
17546
  const embeddingOptions = {
17255
17547
  ...ctx.embeddingOptions,
17256
- taskType: "memory_recall_embed"
17548
+ taskType: "memory_recall_embed",
17549
+ ...options.embeddingCache && !ctx.embeddingOptions.cache && {
17550
+ cache: maskScopedEmbeddingCache(
17551
+ options.embeddingCache,
17552
+ Boolean(ctx.embeddingOptions.maskInput)
17553
+ )
17554
+ }
17257
17555
  };
17258
- const t0 = nowMs2();
17556
+ const t0 = nowMs();
17259
17557
  let prepMs = 0;
17260
17558
  let factLaneMs = 0;
17261
17559
  let chunkLaneMs = 0;
17560
+ let chunkStats;
17262
17561
  let fuseMs = 0;
17263
17562
  const factResults = [];
17264
17563
  const chunkResults = [];
@@ -17313,6 +17612,11 @@ async function recall(query, ctx, options = {}) {
17313
17612
  ...decryptLastRan !== void 0 && { decryptLast: decryptLastRan },
17314
17613
  ...vaultRowsDecrypted !== void 0 && { vaultRowsDecrypted },
17315
17614
  ...vaultRowsEmbedded !== void 0 && { vaultRowsEmbedded },
17615
+ ...chunkStats && {
17616
+ chunkRowsScanned: chunkStats.rowsScanned,
17617
+ chunkRowsFetched: chunkStats.rowsFetched,
17618
+ chunkCacheHits: chunkStats.cacheHits
17619
+ },
17316
17620
  factCount: factResults.length,
17317
17621
  chunkCount: chunkResults.length,
17318
17622
  admittedCount: admitted.length,
@@ -17324,7 +17628,7 @@ async function recall(query, ctx, options = {}) {
17324
17628
  temporalLaneCount,
17325
17629
  emptyReason: emptyReasonFor(admitted.length, laneRan),
17326
17630
  timings: {
17327
- total: nowMs2() - t0,
17631
+ total: nowMs() - t0,
17328
17632
  prep: prepMs,
17329
17633
  factLane: factLaneMs,
17330
17634
  rerank: rerankMs,
@@ -17346,14 +17650,14 @@ async function recall(query, ctx, options = {}) {
17346
17650
  const needsChunkEmbedding = types.includes("chunk") && ctx.storageCtx;
17347
17651
  const wantsTemporal = types.includes("fact") && (ctx.factSource || ctx.vaultCtx);
17348
17652
  const queryEmbedTotalTimeoutMs = options.queryEmbedTotalTimeoutMs ?? DEFAULT_QUERY_EMBED_TOTAL_TIMEOUT_MS;
17349
- const prepStart = nowMs2();
17653
+ const prepStart = nowMs();
17350
17654
  let sharedEmbedMs = 0;
17351
17655
  const [queryEmbedding, entityRanking, temporalRanking] = await Promise.all([
17352
17656
  needsChunkEmbedding ? generateEmbedding(query, {
17353
17657
  ...embeddingOptions,
17354
17658
  totalTimeoutMs: queryEmbedTotalTimeoutMs
17355
17659
  }).finally(() => {
17356
- sharedEmbedMs = nowMs2() - prepStart;
17660
+ sharedEmbedMs = nowMs() - prepStart;
17357
17661
  }).then((vec) => {
17358
17662
  if (vec.length === 0) {
17359
17663
  getLogger().warn(
@@ -17386,11 +17690,11 @@ async function recall(query, ctx, options = {}) {
17386
17690
  () => ctx.factSource ? ctx.factSource.temporalRanking(query, options.now) : buildTemporalLaneRanking(query, ctx.vaultCtx, options.now)
17387
17691
  ) : Promise.resolve([])
17388
17692
  ]);
17389
- prepMs = nowMs2() - prepStart;
17693
+ prepMs = nowMs() - prepStart;
17390
17694
  graphLaneCount = entityRanking.length;
17391
17695
  temporalLaneCount = temporalRanking.length;
17392
17696
  if (types.includes("fact") && (ctx.factSource || ctx.vaultCtx && ctx.vaultCache)) {
17393
- const factStart = nowMs2();
17697
+ const factStart = nowMs();
17394
17698
  const vaultMinScore = options.minScore ?? DEFAULT_FACT_MIN_SCORE;
17395
17699
  factFloor = vaultMinScore;
17396
17700
  const {
@@ -17466,11 +17770,11 @@ async function recall(query, ctx, options = {}) {
17466
17770
  factLaneRankedOnCosine = factRankedOnCosine;
17467
17771
  decryptLastRan = factDecryptLast;
17468
17772
  vaultRowsDecrypted = factRowsDecrypted;
17469
- factLaneMs = nowMs2() - factStart;
17773
+ factLaneMs = nowMs() - factStart;
17470
17774
  }
17471
17775
  if (chunkEmbedFailed && !factLaneRankedOnCosine) embeddingsUnavailable = true;
17472
17776
  if (types.includes("chunk") && ctx.storageCtx && queryEmbedding) {
17473
- const chunkStart = nowMs2();
17777
+ const chunkStart = nowMs();
17474
17778
  const chunkMinScore = options.minScore ?? DEFAULT_CHUNK_MIN_SCORE;
17475
17779
  chunkFloor = chunkMinScore;
17476
17780
  const results = await searchChunksOp(ctx.storageCtx, queryEmbedding, {
@@ -17481,12 +17785,15 @@ async function recall(query, ctx, options = {}) {
17481
17785
  ...options.excludeConversationId && {
17482
17786
  excludeConversationId: options.excludeConversationId
17483
17787
  },
17484
- ...ctx.chunkCache && { chunkCache: ctx.chunkCache }
17788
+ ...ctx.chunkCache && { chunkCache: ctx.chunkCache },
17789
+ onStats: (stats) => {
17790
+ chunkStats = stats;
17791
+ }
17485
17792
  });
17486
17793
  chunkResults.push(...dedupeBy(results, (r) => r.chunkText.trim()));
17487
- chunkLaneMs = nowMs2() - chunkStart;
17794
+ chunkLaneMs = nowMs() - chunkStart;
17488
17795
  }
17489
- const fuseStart = nowMs2();
17796
+ const fuseStart = nowMs();
17490
17797
  if (types.length === 1 || factResults.length === 0 || chunkResults.length === 0) {
17491
17798
  const memories2 = [
17492
17799
  ...factResults.map((r) => toFactMemory(r, options.now)),
@@ -17494,7 +17801,7 @@ async function recall(query, ctx, options = {}) {
17494
17801
  ];
17495
17802
  memories2.sort((a, b) => b.score - a.score);
17496
17803
  const candidateCount = factResults.length + chunkResults.length;
17497
- fuseMs = nowMs2() - fuseStart;
17804
+ fuseMs = nowMs() - fuseStart;
17498
17805
  const admitted = memories2.slice(0, limit);
17499
17806
  hitLimit = memories2.length > limit;
17500
17807
  emitDiagnostics(candidateCount, admitted);
@@ -17558,7 +17865,7 @@ async function recall(query, ctx, options = {}) {
17558
17865
  memories = selection.out;
17559
17866
  }
17560
17867
  hitLimit = selection.cut;
17561
- fuseMs = nowMs2() - fuseStart;
17868
+ fuseMs = nowMs() - fuseStart;
17562
17869
  emitDiagnostics(byId.size, memories);
17563
17870
  return {
17564
17871
  memories,
@@ -25169,11 +25476,12 @@ function storedToLlmapiMessage(stored2) {
25169
25476
  }
25170
25477
  }
25171
25478
  const messages = [];
25172
- if (stored2.role === "assistant" && stored2.toolCallEvents?.length) {
25479
+ const toolCallEvents = Array.isArray(stored2.toolCallEvents) ? stored2.toolCallEvents.filter((event) => !hasEncryptedToolPayload(event)) : [];
25480
+ if (stored2.role === "assistant" && toolCallEvents.length) {
25173
25481
  messages.push({
25174
25482
  role: stored2.role,
25175
25483
  content: void 0,
25176
- tool_calls: stored2.toolCallEvents.map((event) => ({
25484
+ tool_calls: toolCallEvents.map((event) => ({
25177
25485
  id: event.id,
25178
25486
  type: "function",
25179
25487
  function: {
@@ -25182,7 +25490,7 @@ function storedToLlmapiMessage(stored2) {
25182
25490
  }
25183
25491
  }))
25184
25492
  });
25185
- for (const event of stored2.toolCallEvents) {
25493
+ for (const event of toolCallEvents) {
25186
25494
  if (event.id && event.output !== void 0 && event.output !== null) {
25187
25495
  messages.push({
25188
25496
  role: "tool",
@@ -27185,6 +27493,7 @@ export {
27185
27493
  embedAllMessages,
27186
27494
  embedMessage,
27187
27495
  encodeChunkVector,
27496
+ encryptToolCallEventsOp,
27188
27497
  ensureDefaultFoldersOp,
27189
27498
  exchangeNotionCode,
27190
27499
  extractAndLinkEntitiesForMemoriesOp,