@anuma/sdk 7.0.1 → 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) {
@@ -2647,6 +2676,95 @@ async function readRawJsonField(raw, column, walletAddress) {
2647
2676
  return void 0;
2648
2677
  }
2649
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
+ }
2650
2768
  async function searchChunksOp(ctx, queryVector, options) {
2651
2769
  const {
2652
2770
  limit = 10,
@@ -2654,83 +2772,111 @@ async function searchChunksOp(ctx, queryVector, options) {
2654
2772
  conversationId,
2655
2773
  excludeConversationId,
2656
2774
  embeddingModel,
2657
- chunkCache
2775
+ chunkCache,
2776
+ onStats
2658
2777
  } = options || {};
2659
- const activeConversations = await ctx.conversationsCollection.query(Q.where("is_deleted", false)).fetch();
2660
- const activeConversationIds = new Set(
2661
- activeConversations.map((c) => String(c._getRaw("conversation_id")))
2662
- );
2663
- const queryConditions = conversationId ? [Q.where("conversation_id", conversationId)] : [];
2664
- 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);
2665
2780
  const candidates = [];
2666
2781
  let staleSkipped = 0;
2667
2782
  let unreadableVectors = 0;
2668
- for (const message of messages) {
2669
- const msgConvId = String(message.conversation_id);
2670
- if (!activeConversationIds.has(msgConvId)) continue;
2671
- if (excludeConversationId !== void 0 && msgConvId === excludeConversationId) continue;
2672
- if (embeddingModel) {
2673
- const storedModel = message.embedding_model;
2674
- if (storedModel !== null && storedModel !== void 0 && storedModel !== embeddingModel) {
2675
- staleSkipped++;
2676
- continue;
2677
- }
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;
2678
2792
  }
2679
- const version = Number(message.updated_at) || 0;
2680
- const cached = chunkCache?.get(message.id);
2681
- if (cached && cached.version === version) {
2682
- if (cached.chunks.length > 0) {
2683
- for (let ci = 0; ci < cached.chunks.length; ci++) {
2684
- const vec = cached.chunks[ci];
2685
- if (vec.length === 0) continue;
2686
- const similarity = cosineSimilarity(queryVector, vec);
2687
- if (similarity >= minSimilarity) {
2688
- candidates.push({
2689
- message,
2690
- similarity,
2691
- chunkTextSource: { kind: "chunk-cached", chunkIndex: ci, version }
2692
- });
2693
- }
2694
- }
2695
- } else if (cached.fallback) {
2696
- const similarity = cosineSimilarity(queryVector, cached.fallback);
2697
- if (similarity >= minSimilarity) {
2698
- candidates.push({ message, similarity, chunkTextSource: { kind: "message" } });
2699
- }
2700
- }
2793
+ const cached = chunkCache?.get(key.id);
2794
+ if (!cached || cached.version !== key.version) {
2795
+ missIds.push(key.id);
2701
2796
  continue;
2702
2797
  }
2703
- const chunks3 = await readRawJsonField(message, "chunks", ctx.walletAddress);
2704
- if (chunks3 && chunks3.length > 0) {
2705
- const vectors = chunks3.map(
2706
- (chunk) => decodeChunkVector(chunk.vector, () => {
2707
- unreadableVectors++;
2708
- })
2709
- );
2710
- chunkCache?.set(message.id, { version, chunks: vectors });
2711
- for (let ci = 0; ci < chunks3.length; ci++) {
2712
- 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];
2713
2802
  if (vec.length === 0) continue;
2714
2803
  const similarity = cosineSimilarity(queryVector, vec);
2715
2804
  if (similarity >= minSimilarity) {
2716
2805
  candidates.push({
2717
- message,
2806
+ id: key.id,
2807
+ version: key.version,
2808
+ order,
2718
2809
  similarity,
2719
- chunkTextSource: { kind: "chunk", chunk: chunks3[ci], siblings: chunks3 }
2810
+ chunkTextSource: { kind: "chunk-cached", chunkIndex: ci, version: key.version }
2720
2811
  });
2721
2812
  }
2722
2813
  }
2723
- } else {
2724
- const messageVector = await readRawJsonField(message, "vector", ctx.walletAddress);
2725
- if (!messageVector || messageVector.length === 0) continue;
2726
- const fallback = Float32Array.from(messageVector);
2727
- chunkCache?.set(message.id, { version, chunks: [], fallback });
2728
- const similarity = cosineSimilarity(queryVector, fallback);
2814
+ } else if (cached.fallback) {
2815
+ const similarity = cosineSimilarity(queryVector, cached.fallback);
2729
2816
  if (similarity >= minSimilarity) {
2730
- 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
+ });
2824
+ }
2825
+ }
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
+ }
2731
2876
  }
2732
2877
  }
2733
2878
  }
2879
+ onStats?.({ rowsScanned: source.keys.length, rowsFetched, cacheHits });
2734
2880
  if (staleSkipped > 0) {
2735
2881
  getLogger().warn(
2736
2882
  `searchChunksOp: skipped ${staleSkipped} messages whose embedding model differs from the current model (${embeddingModel}) \u2014 re-embed via chunkAndEmbedAllMessages`
@@ -2741,12 +2887,17 @@ async function searchChunksOp(ctx, queryVector, options) {
2741
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`
2742
2888
  );
2743
2889
  }
2744
- const topK = candidates.sort((a, b) => b.similarity - a.similarity).slice(0, limit);
2745
- const uniqueMessages = /* @__PURE__ */ new Map();
2746
- 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
+ });
2747
2898
  const storedById = new Map(
2748
2899
  await Promise.all(
2749
- Array.from(uniqueMessages, async ([id, m]) => {
2900
+ Array.from(rowById, async ([id, m]) => {
2750
2901
  const stored2 = await messageRawToStored(
2751
2902
  m,
2752
2903
  ctx.walletAddress,
@@ -2759,24 +2910,24 @@ async function searchChunksOp(ctx, queryVector, options) {
2759
2910
  );
2760
2911
  const chunkStateByMsgId = /* @__PURE__ */ new Map();
2761
2912
  const needsChunkText = /* @__PURE__ */ new Set();
2762
- for (const c of topK) {
2763
- 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);
2764
2915
  }
2765
2916
  await Promise.all(
2766
2917
  Array.from(needsChunkText, async (id) => {
2767
- const m = uniqueMessages.get(id);
2918
+ const m = rowById.get(id);
2768
2919
  const parsed = await readRawJsonField(m, "chunks", ctx.walletAddress);
2769
2920
  const version = Number(m.updated_at) || 0;
2770
2921
  if (parsed) chunkStateByMsgId.set(id, { version, chunks: parsed });
2771
2922
  })
2772
2923
  );
2773
- return topK.map(({ message, similarity, chunkTextSource }) => {
2774
- const stored2 = { ...storedById.get(message.id) };
2924
+ return liveTopK.map(({ id, similarity, chunkTextSource }) => {
2925
+ const stored2 = { ...storedById.get(id) };
2775
2926
  let chunkText2;
2776
2927
  if (chunkTextSource.kind === "chunk") {
2777
2928
  chunkText2 = resolveChunkText(stored2.content, chunkTextSource.chunk, chunkTextSource.siblings);
2778
2929
  } else if (chunkTextSource.kind === "chunk-cached") {
2779
- const state = chunkStateByMsgId.get(message.id);
2930
+ const state = chunkStateByMsgId.get(id);
2780
2931
  const chunk = state && state.version === chunkTextSource.version ? state.chunks[chunkTextSource.chunkIndex] : void 0;
2781
2932
  chunkText2 = resolveChunkText(stored2.content, chunk, state?.chunks);
2782
2933
  } else {
@@ -3855,13 +4006,35 @@ async function generateEmbedding(text14, options) {
3855
4006
  if (total === void 0) return embedOne(text14, options, void 0);
3856
4007
  return withDeadline((signal) => embedOne(text14, options, signal), total, "embedding");
3857
4008
  }
4009
+ var inFlightByCache = /* @__PURE__ */ new WeakMap();
3858
4010
  async function embedOne(text14, options, outer) {
3859
- const { baseUrl = BASE_URL, model, cache } = options;
3860
- const timeoutMs = options.timeoutMs ?? DEFAULT_EMBEDDING_REQUEST_TIMEOUT_MS;
3861
- if (cache) {
3862
- const cached = cache.get(text14);
3863
- 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
+ }
3864
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);
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;
3865
4038
  const headers = {
3866
4039
  ...await resolveAuthHeaders(options, outer),
3867
4040
  ...taskTypeHeader(options.taskType)
@@ -3896,11 +4069,7 @@ async function embedOne(text14, options, outer) {
3896
4069
  totalTokens: response.data.usage.total_tokens ?? 0
3897
4070
  });
3898
4071
  }
3899
- const f32Embedding = Float32Array.from(embedding2);
3900
- if (cache) {
3901
- cache.set(text14, f32Embedding);
3902
- }
3903
- return Array.from(f32Embedding);
4072
+ return Float32Array.from(embedding2);
3904
4073
  }
3905
4074
  var DEFAULT_EMBEDDING_BATCH_SIZE = 100;
3906
4075
  var DEFAULT_EMBEDDING_BATCH_CONCURRENCY = 3;
@@ -9382,8 +9551,12 @@ var MaskScopedEmbeddingCache = class extends Map {
9382
9551
  return this;
9383
9552
  }
9384
9553
  };
9554
+ var scopedViews = /* @__PURE__ */ new WeakMap();
9385
9555
  function maskScopedEmbeddingCache(cache, masked) {
9386
- 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));
9387
9560
  }
9388
9561
 
9389
9562
  // src/lib/db/conversationMemory/models.ts
@@ -13727,9 +13900,9 @@ function recencyMultiplier(updatedAt, opts = {}) {
13727
13900
  const updatedMs = updatedAt.getTime();
13728
13901
  if (!Number.isFinite(updatedMs)) return noDate;
13729
13902
  const now = opts.now ?? /* @__PURE__ */ new Date();
13730
- const nowMs3 = now.getTime();
13731
- if (!Number.isFinite(nowMs3)) return noDate;
13732
- 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);
13733
13906
  const perYearDecay = opts.perYearDecay ?? DEFAULT_PER_YEAR_DECAY;
13734
13907
  const floor = opts.floor ?? DEFAULT_FLOOR;
13735
13908
  const decay = perYearDecay * (ageDays2 / 365);
@@ -14238,9 +14411,6 @@ function createVaultEmbeddingCache(maxSize = DEFAULT_VAULT_CACHE_SIZE) {
14238
14411
 
14239
14412
  // src/lib/memoryVault/searchTool.ts
14240
14413
  var DEFAULT_RERANK_TOP_N = 5;
14241
- function nowMs() {
14242
- return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
14243
- }
14244
14414
  var warnedProgrammaticDecomposeIgnored = false;
14245
14415
  function normalizeEventTimeKind(kind) {
14246
14416
  return kind === "point" || kind === "range" || kind === "ongoing" ? kind : null;
@@ -17338,9 +17508,6 @@ function scoreEventTimeOverlap(memoryStart, memoryEnd, memoryKind, window2) {
17338
17508
  var DEFAULT_LIMIT = 8;
17339
17509
  var DEFAULT_BUDGET = "low";
17340
17510
  var DEFAULT_FACT_MIN_SCORE = 0.1;
17341
- function nowMs2() {
17342
- return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
17343
- }
17344
17511
  var DEFAULT_CHUNK_MIN_SCORE = 0.5;
17345
17512
  var DEFAULT_QUERY_EMBED_TOTAL_TIMEOUT_MS = 8e3;
17346
17513
  function flagsForBudget(budget) {
@@ -17378,12 +17545,19 @@ async function recall(query, ctx, options = {}) {
17378
17545
  const subQueries = normalizedFacets.length >= 2 ? normalizedFacets : void 0;
17379
17546
  const embeddingOptions = {
17380
17547
  ...ctx.embeddingOptions,
17381
- 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
+ }
17382
17555
  };
17383
- const t0 = nowMs2();
17556
+ const t0 = nowMs();
17384
17557
  let prepMs = 0;
17385
17558
  let factLaneMs = 0;
17386
17559
  let chunkLaneMs = 0;
17560
+ let chunkStats;
17387
17561
  let fuseMs = 0;
17388
17562
  const factResults = [];
17389
17563
  const chunkResults = [];
@@ -17438,6 +17612,11 @@ async function recall(query, ctx, options = {}) {
17438
17612
  ...decryptLastRan !== void 0 && { decryptLast: decryptLastRan },
17439
17613
  ...vaultRowsDecrypted !== void 0 && { vaultRowsDecrypted },
17440
17614
  ...vaultRowsEmbedded !== void 0 && { vaultRowsEmbedded },
17615
+ ...chunkStats && {
17616
+ chunkRowsScanned: chunkStats.rowsScanned,
17617
+ chunkRowsFetched: chunkStats.rowsFetched,
17618
+ chunkCacheHits: chunkStats.cacheHits
17619
+ },
17441
17620
  factCount: factResults.length,
17442
17621
  chunkCount: chunkResults.length,
17443
17622
  admittedCount: admitted.length,
@@ -17449,7 +17628,7 @@ async function recall(query, ctx, options = {}) {
17449
17628
  temporalLaneCount,
17450
17629
  emptyReason: emptyReasonFor(admitted.length, laneRan),
17451
17630
  timings: {
17452
- total: nowMs2() - t0,
17631
+ total: nowMs() - t0,
17453
17632
  prep: prepMs,
17454
17633
  factLane: factLaneMs,
17455
17634
  rerank: rerankMs,
@@ -17471,14 +17650,14 @@ async function recall(query, ctx, options = {}) {
17471
17650
  const needsChunkEmbedding = types.includes("chunk") && ctx.storageCtx;
17472
17651
  const wantsTemporal = types.includes("fact") && (ctx.factSource || ctx.vaultCtx);
17473
17652
  const queryEmbedTotalTimeoutMs = options.queryEmbedTotalTimeoutMs ?? DEFAULT_QUERY_EMBED_TOTAL_TIMEOUT_MS;
17474
- const prepStart = nowMs2();
17653
+ const prepStart = nowMs();
17475
17654
  let sharedEmbedMs = 0;
17476
17655
  const [queryEmbedding, entityRanking, temporalRanking] = await Promise.all([
17477
17656
  needsChunkEmbedding ? generateEmbedding(query, {
17478
17657
  ...embeddingOptions,
17479
17658
  totalTimeoutMs: queryEmbedTotalTimeoutMs
17480
17659
  }).finally(() => {
17481
- sharedEmbedMs = nowMs2() - prepStart;
17660
+ sharedEmbedMs = nowMs() - prepStart;
17482
17661
  }).then((vec) => {
17483
17662
  if (vec.length === 0) {
17484
17663
  getLogger().warn(
@@ -17511,11 +17690,11 @@ async function recall(query, ctx, options = {}) {
17511
17690
  () => ctx.factSource ? ctx.factSource.temporalRanking(query, options.now) : buildTemporalLaneRanking(query, ctx.vaultCtx, options.now)
17512
17691
  ) : Promise.resolve([])
17513
17692
  ]);
17514
- prepMs = nowMs2() - prepStart;
17693
+ prepMs = nowMs() - prepStart;
17515
17694
  graphLaneCount = entityRanking.length;
17516
17695
  temporalLaneCount = temporalRanking.length;
17517
17696
  if (types.includes("fact") && (ctx.factSource || ctx.vaultCtx && ctx.vaultCache)) {
17518
- const factStart = nowMs2();
17697
+ const factStart = nowMs();
17519
17698
  const vaultMinScore = options.minScore ?? DEFAULT_FACT_MIN_SCORE;
17520
17699
  factFloor = vaultMinScore;
17521
17700
  const {
@@ -17591,11 +17770,11 @@ async function recall(query, ctx, options = {}) {
17591
17770
  factLaneRankedOnCosine = factRankedOnCosine;
17592
17771
  decryptLastRan = factDecryptLast;
17593
17772
  vaultRowsDecrypted = factRowsDecrypted;
17594
- factLaneMs = nowMs2() - factStart;
17773
+ factLaneMs = nowMs() - factStart;
17595
17774
  }
17596
17775
  if (chunkEmbedFailed && !factLaneRankedOnCosine) embeddingsUnavailable = true;
17597
17776
  if (types.includes("chunk") && ctx.storageCtx && queryEmbedding) {
17598
- const chunkStart = nowMs2();
17777
+ const chunkStart = nowMs();
17599
17778
  const chunkMinScore = options.minScore ?? DEFAULT_CHUNK_MIN_SCORE;
17600
17779
  chunkFloor = chunkMinScore;
17601
17780
  const results = await searchChunksOp(ctx.storageCtx, queryEmbedding, {
@@ -17606,12 +17785,15 @@ async function recall(query, ctx, options = {}) {
17606
17785
  ...options.excludeConversationId && {
17607
17786
  excludeConversationId: options.excludeConversationId
17608
17787
  },
17609
- ...ctx.chunkCache && { chunkCache: ctx.chunkCache }
17788
+ ...ctx.chunkCache && { chunkCache: ctx.chunkCache },
17789
+ onStats: (stats) => {
17790
+ chunkStats = stats;
17791
+ }
17610
17792
  });
17611
17793
  chunkResults.push(...dedupeBy(results, (r) => r.chunkText.trim()));
17612
- chunkLaneMs = nowMs2() - chunkStart;
17794
+ chunkLaneMs = nowMs() - chunkStart;
17613
17795
  }
17614
- const fuseStart = nowMs2();
17796
+ const fuseStart = nowMs();
17615
17797
  if (types.length === 1 || factResults.length === 0 || chunkResults.length === 0) {
17616
17798
  const memories2 = [
17617
17799
  ...factResults.map((r) => toFactMemory(r, options.now)),
@@ -17619,7 +17801,7 @@ async function recall(query, ctx, options = {}) {
17619
17801
  ];
17620
17802
  memories2.sort((a, b) => b.score - a.score);
17621
17803
  const candidateCount = factResults.length + chunkResults.length;
17622
- fuseMs = nowMs2() - fuseStart;
17804
+ fuseMs = nowMs() - fuseStart;
17623
17805
  const admitted = memories2.slice(0, limit);
17624
17806
  hitLimit = memories2.length > limit;
17625
17807
  emitDiagnostics(candidateCount, admitted);
@@ -17683,7 +17865,7 @@ async function recall(query, ctx, options = {}) {
17683
17865
  memories = selection.out;
17684
17866
  }
17685
17867
  hitLimit = selection.cut;
17686
- fuseMs = nowMs2() - fuseStart;
17868
+ fuseMs = nowMs() - fuseStart;
17687
17869
  emitDiagnostics(byId.size, memories);
17688
17870
  return {
17689
17871
  memories,
package/dist/index.cjs CHANGED
@@ -2177,13 +2177,35 @@ async function generateEmbedding(text2, options) {
2177
2177
  if (total === void 0) return embedOne(text2, options, void 0);
2178
2178
  return withDeadline((signal) => embedOne(text2, options, signal), total, "embedding");
2179
2179
  }
2180
+ var inFlightByCache = /* @__PURE__ */ new WeakMap();
2180
2181
  async function embedOne(text2, options, outer) {
2181
- const { baseUrl = BASE_URL, model, cache } = options;
2182
- const timeoutMs = options.timeoutMs ?? DEFAULT_EMBEDDING_REQUEST_TIMEOUT_MS;
2183
- if (cache) {
2184
- const cached = cache.get(text2);
2185
- if (cached) return Array.from(cached);
2182
+ const { cache } = options;
2183
+ if (!cache) return Array.from(await requestEmbedding(text2, options, outer));
2184
+ const cached = cache.get(text2);
2185
+ if (cached) return Array.from(cached);
2186
+ const inFlight = inFlightByCache.get(cache) ?? /* @__PURE__ */ new Map();
2187
+ inFlightByCache.set(cache, inFlight);
2188
+ const pending = inFlight.get(text2);
2189
+ if (pending) {
2190
+ try {
2191
+ return Array.from(await pending);
2192
+ } catch (err) {
2193
+ if (outer?.aborted) throw err;
2194
+ }
2195
+ }
2196
+ const request = requestEmbedding(text2, options, outer);
2197
+ inFlight.set(text2, request);
2198
+ try {
2199
+ const embedding = await request;
2200
+ cache.set(text2, embedding);
2201
+ return Array.from(embedding);
2202
+ } finally {
2203
+ if (inFlight.get(text2) === request) inFlight.delete(text2);
2186
2204
  }
2205
+ }
2206
+ async function requestEmbedding(text2, options, outer) {
2207
+ const { baseUrl = BASE_URL, model } = options;
2208
+ const timeoutMs = options.timeoutMs ?? DEFAULT_EMBEDDING_REQUEST_TIMEOUT_MS;
2187
2209
  const headers = {
2188
2210
  ...await resolveAuthHeaders(options, outer),
2189
2211
  ...taskTypeHeader(options.taskType)
@@ -2218,11 +2240,7 @@ async function embedOne(text2, options, outer) {
2218
2240
  totalTokens: response.data.usage.total_tokens ?? 0
2219
2241
  });
2220
2242
  }
2221
- const f32Embedding = Float32Array.from(embedding);
2222
- if (cache) {
2223
- cache.set(text2, f32Embedding);
2224
- }
2225
- return Array.from(f32Embedding);
2243
+ return Float32Array.from(embedding);
2226
2244
  }
2227
2245
  var DEFAULT_EMBEDDING_BATCH_SIZE = 100;
2228
2246
  var DEFAULT_EMBEDDING_BATCH_CONCURRENCY = 3;