@anuma/sdk 6.43.0 → 6.44.0

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.
@@ -122,6 +122,18 @@ var init_clientConfig = __esm({
122
122
  }
123
123
  });
124
124
 
125
+ // src/lib/taskType.ts
126
+ function taskTypeHeader(taskType) {
127
+ return taskType ? { [TASK_TYPE_HEADER]: taskType } : {};
128
+ }
129
+ var TASK_TYPE_HEADER;
130
+ var init_taskType = __esm({
131
+ "src/lib/taskType.ts"() {
132
+ "use strict";
133
+ TASK_TYPE_HEADER = "X-Anuma-Task-Type";
134
+ }
135
+ });
136
+
125
137
  // src/lib/pii/patterns.ts
126
138
  function luhnCheck(digits) {
127
139
  let sum = 0;
@@ -766,18 +778,6 @@ var init_internalFlowMarker = __esm({
766
778
  }
767
779
  });
768
780
 
769
- // src/lib/taskType.ts
770
- function taskTypeHeader(taskType) {
771
- return taskType ? { [TASK_TYPE_HEADER]: taskType } : {};
772
- }
773
- var TASK_TYPE_HEADER;
774
- var init_taskType = __esm({
775
- "src/lib/taskType.ts"() {
776
- "use strict";
777
- TASK_TYPE_HEADER = "X-Anuma-Task-Type";
778
- }
779
- });
780
-
781
781
  // src/lib/memory/portalLlm.ts
782
782
  function defaultBaseUrl() {
783
783
  return typeof process !== "undefined" && process.env?.ANUMA_PORTAL_BASE_URL || BASE_URL;
@@ -5218,6 +5218,7 @@ var getApiV1Models = (options) => (options?.client ?? client).get({ url: "/api/v
5218
5218
 
5219
5219
  // src/lib/memoryEngine/generate.ts
5220
5220
  init_clientConfig();
5221
+ init_taskType();
5221
5222
  var EMBED_MAX_ATTEMPTS = 4;
5222
5223
  var DEFAULT_EMBEDDING_REQUEST_TIMEOUT_MS = 15e3;
5223
5224
  var DEFAULT_EMBEDDING_TOKEN_TIMEOUT_MS = 1e4;
@@ -5315,7 +5316,10 @@ async function embedOne(text14, options, outer) {
5315
5316
  const cached = cache.get(text14);
5316
5317
  if (cached) return Array.from(cached);
5317
5318
  }
5318
- const headers = await resolveAuthHeaders(options, outer);
5319
+ const headers = {
5320
+ ...await resolveAuthHeaders(options, outer),
5321
+ ...taskTypeHeader(options.taskType)
5322
+ };
5319
5323
  const response = await withEmbeddingRetry(
5320
5324
  () => withDeadline(
5321
5325
  (signal) => postApiV1Embeddings({
@@ -5410,7 +5414,10 @@ async function embedMany(texts, options, outer) {
5410
5414
  if (uncachedTexts.length === 0) {
5411
5415
  return results;
5412
5416
  }
5413
- const headers = await resolveAuthHeaders(options, outer);
5417
+ const headers = {
5418
+ ...await resolveAuthHeaders(options, outer),
5419
+ ...taskTypeHeader(options.taskType)
5420
+ };
5414
5421
  if (outer?.aborted) throw outer.reason;
5415
5422
  const embeddingModel = model ?? DEFAULT_API_EMBEDDING_MODEL;
5416
5423
  let newEmbeddings;
@@ -15149,6 +15156,50 @@ function applyMMR(candidates, k, lambda = 0.5) {
15149
15156
  return selected;
15150
15157
  }
15151
15158
 
15159
+ // src/lib/memory/recallTelemetry.ts
15160
+ function notifyMemoryCallback(callback, value) {
15161
+ try {
15162
+ callback?.(value);
15163
+ } catch {
15164
+ return;
15165
+ }
15166
+ }
15167
+ function factRecallDiagnostics(scores, elapsedMs, options = {}) {
15168
+ return {
15169
+ usedBudget: "low",
15170
+ reranked: false,
15171
+ candidateCount: options.candidateCount ?? scores.length,
15172
+ ...options.vaultSize !== void 0 && { vaultSize: options.vaultSize },
15173
+ ...options.decryptLast !== void 0 && { decryptLast: options.decryptLast },
15174
+ ...options.vaultRowsDecrypted !== void 0 && {
15175
+ vaultRowsDecrypted: options.vaultRowsDecrypted
15176
+ },
15177
+ ...options.vaultRowsEmbedded !== void 0 && {
15178
+ vaultRowsEmbedded: options.vaultRowsEmbedded
15179
+ },
15180
+ factCount: scores.length,
15181
+ chunkCount: 0,
15182
+ admittedCount: scores.length,
15183
+ topScore: scores.length ? Math.max(...scores) : -1,
15184
+ lowestAdmittedScore: scores.length ? Math.min(...scores) : -1,
15185
+ minScoreApplied: options.minScore ?? -1,
15186
+ truncated: options.truncated ?? false,
15187
+ graphLaneCount: 0,
15188
+ temporalLaneCount: 0,
15189
+ emptyReason: scores.length ? "" : options.vaultSize === 0 ? "vault-empty" : "no-candidates",
15190
+ timings: {
15191
+ total: elapsedMs,
15192
+ prep: 0,
15193
+ factLane: elapsedMs,
15194
+ rerank: 0,
15195
+ queryEmbed: options.queryEmbedMs ?? 0,
15196
+ chunkLane: 0,
15197
+ fuse: 0
15198
+ },
15199
+ degraded: options.embeddingsUnavailable ? ["embeddings-unavailable"] : []
15200
+ };
15201
+ }
15202
+
15152
15203
  // src/lib/memory/recency.ts
15153
15204
  var DAY_MS2 = 1e3 * 60 * 60 * 24;
15154
15205
  var DEFAULT_PER_YEAR_DECAY = 0.2;
@@ -15841,6 +15892,7 @@ function rankVaultMemories(_query, queryEmbedding, items, options) {
15841
15892
  }
15842
15893
  filtered.sort((a, b) => b.similarity - a.similarity);
15843
15894
  }
15895
+ notifyMemoryCallback(options?.onCandidateCount, filtered.length);
15844
15896
  const itemById = new Map(items.map((i) => [i.id, i]));
15845
15897
  return filtered.slice(0, limit).map((r) => resultFromItem(itemById.get(r.uniqueId), r.similarity));
15846
15898
  }
@@ -16823,6 +16875,7 @@ async function rankPreparedVaultCandidates(query, prepared, embeddingOptions, se
16823
16875
  const results = rankVaultMemories(query, queryEmbedding, embeddedItems, {
16824
16876
  limit,
16825
16877
  minSimilarity,
16878
+ onCandidateCount: searchOptions?.onCandidateCount,
16826
16879
  ...searchOptions?.supersessionBoost !== void 0 && {
16827
16880
  supersessionBoost: searchOptions.supersessionBoost
16828
16881
  },
@@ -16924,16 +16977,20 @@ var DEFAULT_SCOPE = "private";
16924
16977
  var DEFAULT_CONSOLIDATE_THRESHOLD = 0.55;
16925
16978
  var DEFAULT_CONSOLIDATE_TOP_K = 20;
16926
16979
  async function retain(content, ctx, options = {}) {
16980
+ const embeddingOptions = {
16981
+ ...ctx.embeddingOptions,
16982
+ taskType: "memory_retain_embed"
16983
+ };
16927
16984
  return retainWithPersistence(
16928
16985
  content,
16929
16986
  {
16930
- embeddingOptions: ctx.embeddingOptions,
16987
+ embeddingOptions,
16931
16988
  vaultCache: ctx.vaultCache,
16932
16989
  persistence: {
16933
16990
  prepare: (query, searchOptions) => prepareVaultCandidates(
16934
16991
  query,
16935
16992
  ctx.vaultCtx,
16936
- ctx.embeddingOptions,
16993
+ embeddingOptions,
16937
16994
  ctx.vaultCache,
16938
16995
  searchOptions
16939
16996
  ),
@@ -18087,7 +18144,17 @@ function parseVerdict(parsed) {
18087
18144
 
18088
18145
  // src/lib/memory/decayWorker.ts
18089
18146
  init_logger();
18090
- var EMPTY_RESULT = { archived: 0, deleted: 0, scanned: 0 };
18147
+ function emptyResult() {
18148
+ return {
18149
+ archived: 0,
18150
+ deleted: 0,
18151
+ scanned: 0,
18152
+ ruleDriven: { archived: 0, deleted: 0 },
18153
+ classifierDriven: { archived: 0, deleted: 0 },
18154
+ classifierCalls: 0,
18155
+ classifierCapHits: 0
18156
+ };
18157
+ }
18091
18158
  var DEFAULT_MAX_CLASSIFIER_CALLS_PER_SWEEP = 20;
18092
18159
  function resolveNow(now) {
18093
18160
  if (typeof now === "function") return now();
@@ -18126,42 +18193,45 @@ function createDecaySweeper(options) {
18126
18193
  let disposed = false;
18127
18194
  async function verdictFor(input, now, sweep) {
18128
18195
  const ruleVerdict = classifyDecay(input, now, policy);
18129
- if (ruleVerdict !== "keep") return ruleVerdict;
18130
- if (!classifier || !isBorderline(input)) return ruleVerdict;
18196
+ const rule = { verdict: ruleVerdict, source: "ruleDriven" };
18197
+ if (ruleVerdict !== "keep") return rule;
18198
+ if (!classifier || !isBorderline(input)) return rule;
18131
18199
  if (input.id) {
18132
18200
  const cached = classifierCache.get(input.id);
18133
- if (cached && cached.activityAt === lastActivityAt(input)) return cached.verdict;
18201
+ if (cached && cached.activityAt === lastActivityAt(input))
18202
+ return { verdict: cached.verdict, source: "classifierDriven" };
18134
18203
  }
18135
18204
  if (sweep.classifierCalls >= maxClassifierCalls) {
18205
+ sweep.classifierCapHits++;
18136
18206
  if (!sweep.ceilingLogged) {
18137
18207
  sweep.ceilingLogged = true;
18138
18208
  getLogger().warn(
18139
18209
  `[memory/decay] classifier per-sweep ceiling (${maxClassifierCalls}) reached; remaining borderline rows use the rule verdict this sweep`
18140
18210
  );
18141
18211
  }
18142
- return ruleVerdict;
18212
+ return rule;
18143
18213
  }
18144
18214
  sweep.classifierCalls++;
18145
18215
  try {
18146
18216
  const verdict = await classifier.classify(input, ruleVerdict, now);
18147
18217
  if (input.id) classifierCache.set(input.id, { activityAt: lastActivityAt(input), verdict });
18148
- return verdict;
18218
+ return { verdict, source: "classifierDriven" };
18149
18219
  } catch (err) {
18150
18220
  getLogger().warn(
18151
18221
  `[memory/decay] classifier failed; falling back to rule verdict: ${err instanceof Error ? err.message : String(err)}`
18152
18222
  );
18153
- return ruleVerdict;
18223
+ return rule;
18154
18224
  }
18155
18225
  }
18156
18226
  async function runSweep() {
18157
- if (disposed) return { ...EMPTY_RESULT };
18227
+ if (disposed) return emptyResult();
18158
18228
  const now = resolveNow(options.now);
18159
18229
  let candidates;
18160
18230
  try {
18161
18231
  candidates = await getDecayCandidatesRawOp(vaultCtx);
18162
18232
  } catch (err) {
18163
18233
  onError?.(err instanceof Error ? err : new Error(String(err)));
18164
- return { ...EMPTY_RESULT };
18234
+ return emptyResult();
18165
18235
  }
18166
18236
  if (classifierCache.size > 0) {
18167
18237
  const liveIds = new Set(candidates.map((c) => c.uniqueId));
@@ -18169,33 +18239,40 @@ function createDecaySweeper(options) {
18169
18239
  if (!liveIds.has(id)) classifierCache.delete(id);
18170
18240
  }
18171
18241
  }
18172
- const sweep = { classifierCalls: 0, ceilingLogged: false };
18242
+ const sweep = { classifierCalls: 0, classifierCapHits: 0, ceilingLogged: false };
18173
18243
  const toArchive = [];
18174
18244
  const toDelete = [];
18175
18245
  for (const c of candidates) {
18176
- const verdict = await verdictFor(toDecayInput(c), now, sweep);
18177
- if (verdict === "archive") toArchive.push(c);
18178
- else if (verdict === "delete") toDelete.push(c);
18179
- }
18180
- let archived = 0;
18181
- let deleted = 0;
18246
+ const { verdict, source } = await verdictFor(toDecayInput(c), now, sweep);
18247
+ if (verdict === "archive") toArchive.push({ candidate: c, source });
18248
+ else if (verdict === "delete") toDelete.push({ candidate: c, source });
18249
+ }
18250
+ const result = emptyResult();
18251
+ result.scanned = candidates.length;
18252
+ result.classifierCalls = sweep.classifierCalls;
18253
+ result.classifierCapHits = sweep.classifierCapHits;
18182
18254
  try {
18183
- for (const c of toArchive) {
18255
+ for (const { candidate: c, source } of toArchive) {
18184
18256
  const ok = await archiveVaultMemoryOp(vaultCtx, c.uniqueId, {
18185
18257
  now,
18186
18258
  expectedUpdatedAt: c.updatedAt,
18187
18259
  expectedLastObservedAt: c.lastObservedAt ?? null
18188
18260
  });
18189
- if (ok) archived++;
18261
+ if (ok) {
18262
+ result.archived++;
18263
+ result[source].archived++;
18264
+ }
18190
18265
  }
18191
- for (const c of toDelete) {
18266
+ for (const { candidate: c, source } of toDelete) {
18192
18267
  const ok = await hardDeleteDecayedOp(vaultCtx, c.uniqueId, { hardDeleteWindowMs, now });
18193
- if (ok) deleted++;
18268
+ if (ok) {
18269
+ result.deleted++;
18270
+ result[source].deleted++;
18271
+ }
18194
18272
  }
18195
18273
  } catch (err) {
18196
18274
  onError?.(err instanceof Error ? err : new Error(String(err)));
18197
18275
  }
18198
- const result = { archived, deleted, scanned: candidates.length };
18199
18276
  onSwept?.(result);
18200
18277
  return result;
18201
18278
  }
@@ -18800,6 +18877,10 @@ async function recall(query, ctx, options = {}) {
18800
18877
  const flags = flagsForBudget(usedBudget);
18801
18878
  const normalizedFacets = normalizeSubQueries(options.subQueries);
18802
18879
  const subQueries = normalizedFacets.length >= 2 ? normalizedFacets : void 0;
18880
+ const embeddingOptions = {
18881
+ ...ctx.embeddingOptions,
18882
+ taskType: "memory_recall_embed"
18883
+ };
18803
18884
  const t0 = nowMs2();
18804
18885
  let prepMs = 0;
18805
18886
  let factLaneMs = 0;
@@ -18895,7 +18976,7 @@ async function recall(query, ctx, options = {}) {
18895
18976
  let sharedEmbedMs = 0;
18896
18977
  const [queryEmbedding, entityRanking, temporalRanking] = await Promise.all([
18897
18978
  needsChunkEmbedding ? generateEmbedding(query, {
18898
- ...ctx.embeddingOptions,
18979
+ ...embeddingOptions,
18899
18980
  totalTimeoutMs: queryEmbedTotalTimeoutMs
18900
18981
  }).finally(() => {
18901
18982
  sharedEmbedMs = nowMs2() - prepStart;
@@ -18953,7 +19034,7 @@ async function recall(query, ctx, options = {}) {
18953
19034
  } = await (ctx.factSource?.search.bind(ctx.factSource) ?? ((searchQuery, searchOptions) => searchVaultMemoriesWithSize(
18954
19035
  searchQuery,
18955
19036
  ctx.vaultCtx,
18956
- ctx.embeddingOptions,
19037
+ embeddingOptions,
18957
19038
  ctx.vaultCache,
18958
19039
  searchOptions
18959
19040
  )))(query, {
@@ -19319,7 +19400,7 @@ function isDumpQuery(query) {
19319
19400
  async function listRecentFacts(ctx, limit, toolOptions) {
19320
19401
  const store = toolOptions?.memoryStore;
19321
19402
  const vaultCtx = ctx.vaultCtx;
19322
- if (!vaultCtx && !store) return [];
19403
+ if (!vaultCtx && !store) return { memories: [], truncated: false };
19323
19404
  const read = (ids) => store ? store.list({ memoryIds: ids }) : getVaultMemoriesByIdsOp(vaultCtx, ids);
19324
19405
  const filters = {
19325
19406
  ...toolOptions?.scopes && { scopes: toolOptions.scopes },
@@ -19330,34 +19411,47 @@ async function listRecentFacts(ctx, limit, toolOptions) {
19330
19411
  ...toolOptions?.folderId !== void 0 && { folderId: toolOptions.folderId }
19331
19412
  });
19332
19413
  const readable = [];
19414
+ const readLimit = limit + 1;
19415
+ let batchLimit = limit;
19333
19416
  let start = 0;
19334
- for (let size = limit; readable.length < limit; size *= 2) {
19417
+ let projected = 0;
19418
+ let exhausted = false;
19419
+ for (let size = readLimit; readable.length < readLimit; size *= 2) {
19335
19420
  const ordered = await project(size);
19336
- for (let i = start; i < ordered.length && readable.length < limit; i += limit) {
19337
- const batch = ordered.slice(i, i + limit).map((p) => p.uniqueId);
19421
+ projected = ordered.length;
19422
+ exhausted = !store || ordered.length < size;
19423
+ for (let i = start; i < ordered.length && readable.length < readLimit; ) {
19424
+ const batch = ordered.slice(i, i + batchLimit).map((p) => p.uniqueId);
19338
19425
  const byId = new Map((await read(batch)).map((m) => [m.uniqueId, m]));
19426
+ const previousCount = readable.length;
19339
19427
  for (const id of batch) {
19340
19428
  const m = byId.get(id);
19341
19429
  if (m && !isEncrypted(m.content)) readable.push(m);
19342
- if (readable.length >= limit) break;
19430
+ if (readable.length >= readLimit) break;
19343
19431
  }
19432
+ batchLimit = readable.length > previousCount ? Math.min(limit, readLimit - readable.length) : limit;
19433
+ i += batch.length;
19344
19434
  }
19345
- if (!store || ordered.length < size) break;
19435
+ if (exhausted) break;
19346
19436
  start = ordered.length;
19347
19437
  }
19348
- return readable.map((m) => ({
19349
- id: m.uniqueId,
19350
- kind: "fact",
19351
- content: m.content,
19352
- score: 0,
19353
- folderId: m.folderId,
19354
- eventTimeStart: m.eventTimeStart,
19355
- eventTimeEnd: m.eventTimeEnd,
19356
- eventTimeKind: m.eventTimeKind,
19357
- factType: m.factType,
19358
- createdAt: m.createdAt,
19359
- updatedAt: m.updatedAt
19360
- }));
19438
+ return {
19439
+ memories: readable.slice(0, limit).map((m) => ({
19440
+ id: m.uniqueId,
19441
+ kind: "fact",
19442
+ content: m.content,
19443
+ score: 0,
19444
+ folderId: m.folderId,
19445
+ eventTimeStart: m.eventTimeStart,
19446
+ eventTimeEnd: m.eventTimeEnd,
19447
+ eventTimeKind: m.eventTimeKind,
19448
+ factType: m.factType,
19449
+ createdAt: m.createdAt,
19450
+ updatedAt: m.updatedAt
19451
+ })),
19452
+ ...exhausted && { vaultSize: projected },
19453
+ truncated: readable.length > limit
19454
+ };
19361
19455
  }
19362
19456
  function createRecallTool(ctx, toolOptions, callbacks) {
19363
19457
  if (toolOptions?.memoryStore && toolOptions.folderId !== void 0)
@@ -19396,7 +19490,11 @@ function createRecallTool(ctx, toolOptions, callbacks) {
19396
19490
  },
19397
19491
  executor: async (args) => {
19398
19492
  if (typeof args.query !== "string" || args.query.length === 0) {
19399
- throw new Error("recall_memory: `query` is required and must be a non-empty string.");
19493
+ const error = new Error(
19494
+ "recall_memory: `query` is required and must be a non-empty string."
19495
+ );
19496
+ notifyMemoryCallback(callbacks?.onError, error);
19497
+ throw error;
19400
19498
  }
19401
19499
  const query = args.query;
19402
19500
  const now = Date.now();
@@ -19424,10 +19522,17 @@ function createRecallTool(ctx, toolOptions, callbacks) {
19424
19522
  RECALL_MAX_MEMORIES_PER_CONVERSATION - conversationMemories
19425
19523
  );
19426
19524
  const effectiveLimit = Math.min(requestLimit, remainingTurnBudget, remainingConvBudget);
19525
+ const truncatedByBudget = effectiveLimit < requestLimit;
19526
+ if (truncatedByBudget) {
19527
+ notifyMemoryCallback(callbacks?.onTruncated, {
19528
+ requestedLimit: requestLimit,
19529
+ effectiveLimit,
19530
+ truncatedByBudget
19531
+ });
19532
+ }
19427
19533
  if (effectiveLimit <= 0) {
19428
19534
  return VOLUME_LIMIT_NOTICE;
19429
19535
  }
19430
- const truncatedByBudget = effectiveLimit < requestLimit;
19431
19536
  turnMemories += effectiveLimit;
19432
19537
  conversationMemories += effectiveLimit;
19433
19538
  let surfaced = 0;
@@ -19436,7 +19541,27 @@ function createRecallTool(ctx, toolOptions, callbacks) {
19436
19541
  const wantsRecent = args.sort === "recent" && defaultTypes.includes("fact") && (ctx.vaultCtx !== void 0 || toolOptions?.memoryStore !== void 0);
19437
19542
  let result;
19438
19543
  if (wantsRecent) {
19439
- result = { memories: await listRecentFacts(ctx, effectiveLimit, toolOptions) };
19544
+ const start = Date.now();
19545
+ const recent = await listRecentFacts(ctx, effectiveLimit, toolOptions);
19546
+ result = recent;
19547
+ notifyMemoryCallback(callbacks?.onDiagnostics, {
19548
+ ...factRecallDiagnostics(
19549
+ result.memories.map((m) => m.score),
19550
+ Date.now() - start,
19551
+ {
19552
+ truncated: recent.truncated,
19553
+ ...recent.vaultSize !== void 0 && { vaultSize: recent.vaultSize }
19554
+ }
19555
+ ),
19556
+ truncatedByBudget
19557
+ });
19558
+ if (recent.truncated && !truncatedByBudget) {
19559
+ notifyMemoryCallback(callbacks?.onTruncated, {
19560
+ requestedLimit: requestLimit,
19561
+ effectiveLimit,
19562
+ truncatedByBudget
19563
+ });
19564
+ }
19440
19565
  } else {
19441
19566
  let subQueries;
19442
19567
  if (defaultBudget === "high" && toolOptions?.decomposeOptions) {
@@ -19448,6 +19573,14 @@ function createRecallTool(ctx, toolOptions, callbacks) {
19448
19573
  const recallOpts = {
19449
19574
  onDiagnostics: (d) => {
19450
19575
  recallDegraded = d.degraded;
19576
+ notifyMemoryCallback(callbacks?.onDiagnostics, { ...d, truncatedByBudget });
19577
+ if (d.truncated && !truncatedByBudget) {
19578
+ notifyMemoryCallback(callbacks?.onTruncated, {
19579
+ requestedLimit: requestLimit,
19580
+ effectiveLimit,
19581
+ truncatedByBudget
19582
+ });
19583
+ }
19451
19584
  },
19452
19585
  types: defaultTypes,
19453
19586
  limit: effectiveLimit,
@@ -19491,6 +19624,7 @@ ${TRUNCATION_NOTICE}` : formatted;
19491
19624
  const message = error instanceof Error ? error.message : "Unknown error";
19492
19625
  const wrapped = new Error(`recall_memory: search failed \u2014 ${message}`);
19493
19626
  wrapped.cause = error;
19627
+ notifyMemoryCallback(callbacks?.onError, wrapped);
19494
19628
  throw wrapped;
19495
19629
  } finally {
19496
19630
  const unused = effectiveLimit - surfaced;
@@ -19732,6 +19866,18 @@ When a memory refers to one of these, reuse the EXACT existing name (same spelli
19732
19866
  ` : "";
19733
19867
  for (let i = 0; i < memories.length; i += TOPIC_EXTRACTION_BATCH_SIZE) {
19734
19868
  const batch = memories.slice(i, i + TOPIC_EXTRACTION_BATCH_SIZE);
19869
+ const start = Date.now();
19870
+ let attempts = 0;
19871
+ let failure;
19872
+ const reportBatch = (unansweredCount) => {
19873
+ notifyMemoryCallback(options.onBatch, {
19874
+ memoryIds: batch.map((m) => m.id),
19875
+ attempts,
19876
+ durationMs: Date.now() - start,
19877
+ unansweredCount,
19878
+ ...failure && { failure }
19879
+ });
19880
+ };
19735
19881
  const rows = [];
19736
19882
  for (const m of batch) {
19737
19883
  const content = m.content.slice(0, MAX_CHARS_PER_MEMORY).replace(/\s+/g, " ").trim();
@@ -19753,6 +19899,12 @@ ${listing}
19753
19899
 
19754
19900
  List each memory's named entities.`,
19755
19901
  tag: "memory/topics",
19902
+ onAttempt: (attempt) => {
19903
+ attempts = attempt.attempt;
19904
+ },
19905
+ onFailure: (value) => {
19906
+ failure = value;
19907
+ },
19756
19908
  extra: { max_completion_tokens: MAX_COMPLETION_TOKENS },
19757
19909
  ...options.fetchFn && { fetchFn: options.fetchFn },
19758
19910
  ...options.maxAttempts !== void 0 && { maxAttempts: options.maxAttempts },
@@ -19761,6 +19913,7 @@ List each memory's named entities.`,
19761
19913
  ...options.backoffMs && { backoffMs: options.backoffMs }
19762
19914
  });
19763
19915
  if (parsed === null) {
19916
+ reportBatch(batch.length);
19764
19917
  getLogger().warn(
19765
19918
  `[memory/topics] batch of ${batch.length} failed after retries \u2014 will retry next sweep`
19766
19919
  );
@@ -19769,6 +19922,7 @@ List each memory's named entities.`,
19769
19922
  const validIds = new Set(batch.map((m) => m.id));
19770
19923
  const byId = parseTopicResponse(parsed, validIds);
19771
19924
  if (byId === null) {
19925
+ reportBatch(batch.length);
19772
19926
  getLogger().warn(
19773
19927
  `[memory/topics] batch of ${batch.length} returned an unrecognized shape \u2014 will retry next sweep`
19774
19928
  );
@@ -19781,6 +19935,7 @@ List each memory's named entities.`,
19781
19935
  }
19782
19936
  out.set(m.id, redactor ? restoreEntities(entities, redactor) : entities);
19783
19937
  }
19938
+ reportBatch(batch.filter((m) => !byId.has(m.id)).length);
19784
19939
  }
19785
19940
  return out;
19786
19941
  }
@@ -19819,6 +19974,7 @@ function isDegradedTopicSkip(reason) {
19819
19974
  return DEGRADED_TOPIC_SKIP[reason] === true;
19820
19975
  }
19821
19976
  async function extractAndLinkEntitiesForMemoriesOp(ctx, memoryIds, options) {
19977
+ const start = Date.now();
19822
19978
  const entityCtx = ctx.entityCtx;
19823
19979
  if (!entityCtx) {
19824
19980
  throw new Error("extractAndLinkEntitiesForMemoriesOp requires ctx.entityCtx");
@@ -19859,7 +20015,18 @@ async function extractAndLinkEntitiesForMemoriesOp(ctx, memoryIds, options) {
19859
20015
  skip(id, "unreadable");
19860
20016
  }
19861
20017
  }
19862
- const entitiesByMemory = await extractEntitiesForMemories(inputs, options);
20018
+ const batches = [];
20019
+ const llmFailures = /* @__PURE__ */ new Map();
20020
+ const entitiesByMemory = await extractEntitiesForMemories(inputs, {
20021
+ ...options,
20022
+ onBatch: (batch) => {
20023
+ batches.push(batch);
20024
+ if (batch.failure) {
20025
+ for (const id of batch.memoryIds) llmFailures.set(id, batch.failure);
20026
+ }
20027
+ notifyMemoryCallback(options.onBatch, batch);
20028
+ }
20029
+ });
19863
20030
  const toStamp = [];
19864
20031
  for (const input of inputs) {
19865
20032
  const entities = entitiesByMemory.get(input.id);
@@ -19890,7 +20057,16 @@ async function extractAndLinkEntitiesForMemoriesOp(ctx, memoryIds, options) {
19890
20057
  entitiesByMemory.delete(id);
19891
20058
  }
19892
20059
  }
19893
- return { entitiesByMemory, stampedIds, skippedIds, skippedReasons };
20060
+ return {
20061
+ entitiesByMemory,
20062
+ stampedIds,
20063
+ skippedIds,
20064
+ skippedReasons,
20065
+ batchCount: batches.length,
20066
+ durationMs: Date.now() - start,
20067
+ batches,
20068
+ llmFailures
20069
+ };
19894
20070
  }
19895
20071
 
19896
20072
  // src/lib/memory/store/local.ts
@@ -20045,7 +20221,7 @@ function createLocalMemoryStore(options) {
20045
20221
  search: (query, searchOptions) => searchVaultMemoriesWithSize(
20046
20222
  query,
20047
20223
  vaultCtx,
20048
- options.embeddingOptions,
20224
+ { ...options.embeddingOptions, taskType: "memory_recall_embed" },
20049
20225
  vaultCache,
20050
20226
  searchOptions
20051
20227
  ),
@@ -20709,6 +20885,7 @@ function createRemoteMemoryPipeline(options) {
20709
20885
  const budgetMs = search.queryEmbedTotalTimeoutMs ?? 8e3;
20710
20886
  const embed = (texts) => generateEmbeddings(texts, {
20711
20887
  ...embeddingOptions,
20888
+ taskType: mode === "retain" ? "memory_retain_embed" : "memory_recall_embed",
20712
20889
  totalTimeoutMs: budgetMs
20713
20890
  });
20714
20891
  const embedFacets = async () => {
@@ -20957,7 +21134,7 @@ function createRemoteMemoryPipeline(options) {
20957
21134
  const ranked = await rankPreparedVaultCandidates(
20958
21135
  text14,
20959
21136
  prepared,
20960
- embeddingOptions,
21137
+ { ...embeddingOptions, taskType: "memory_recall_embed" },
20961
21138
  searchOptions
20962
21139
  );
20963
21140
  return {
@@ -20979,7 +21156,7 @@ function createRemoteMemoryPipeline(options) {
20979
21156
  content,
20980
21157
  {
20981
21158
  persistence: port,
20982
- embeddingOptions,
21159
+ embeddingOptions: { ...embeddingOptions, taskType: "memory_retain_embed" },
20983
21160
  vaultCache: /* @__PURE__ */ new Map(),
20984
21161
  normalizeEmbedding: storedVector
20985
21162
  },
@@ -21356,6 +21533,7 @@ var MAX_BATCH = 50;
21356
21533
  var MAX_QUERY_IDS = 1e3;
21357
21534
  var MAX_QUERY_TOPICS = 100;
21358
21535
  var MAX_QUERY_FILTER = 20;
21536
+ var GRAPH_LANE_ROWS = 256;
21359
21537
  var RemoteMemoryPartialCreateError = class extends Error {
21360
21538
  constructor(created, cause) {
21361
21539
  super(`createMany stored ${created.length} memories before a batch failed`);
@@ -21451,7 +21629,11 @@ function createRemoteMemoryStore(options) {
21451
21629
  const { persistence, embeddingOptions } = options;
21452
21630
  const model = embeddingOptions.model ?? DEFAULT_API_EMBEDDING_MODEL;
21453
21631
  const pollIntervalMs = options.pollIntervalMs ?? 6e4;
21454
- const pipeline = createRemoteMemoryPipeline(options);
21632
+ const pipeline = createRemoteMemoryPipeline({
21633
+ ...options,
21634
+ graphRanking: options.graphRanking ?? ((query, _traverse, recallOptions) => topicGraphRanking(query, recallOptions)),
21635
+ temporalRanking: options.temporalRanking ?? (() => Promise.resolve([]))
21636
+ });
21455
21637
  const subscribers = /* @__PURE__ */ new Set();
21456
21638
  const seen = /* @__PURE__ */ new Map();
21457
21639
  let timer;
@@ -21483,6 +21665,36 @@ function createRemoteMemoryStore(options) {
21483
21665
  const decrypted = (options2) => persistence.query(options2);
21484
21666
  const metadata2 = (options2) => persistence.queryMetadata(options2);
21485
21667
  const enumerate = (filters, max = Infinity) => pages(decrypted, filters, max);
21668
+ const memoriesByTopics = async (names) => {
21669
+ const wanted = new Set(names.map(normalizeEntityName).filter(Boolean));
21670
+ const out = /* @__PURE__ */ new Map();
21671
+ const pages2 = await Promise.all(
21672
+ chunks2([...wanted], MAX_QUERY_TOPICS).map((topics) => enumerate({ topics }))
21673
+ );
21674
+ for (const { memory } of pages2.flat()) {
21675
+ const matched = [...canonicalTopics(memory)].filter((name) => wanted.has(name));
21676
+ if (matched.length) out.set(memory.memory_id, new Set(matched));
21677
+ }
21678
+ return out;
21679
+ };
21680
+ const topicGraphRanking = async (query, recallOptions = {}) => {
21681
+ const wanted = new Set(extractQueryEntities(query).map(normalizeEntityName).filter(Boolean));
21682
+ if (wanted.size === 0 || recallOptions.memoryIds?.length === 0) return [];
21683
+ const rows = await pages(
21684
+ metadata2,
21685
+ {
21686
+ topics: [...wanted].slice(0, MAX_QUERY_TOPICS),
21687
+ ...recallOptions.scopes && { scopes: recallOptions.scopes },
21688
+ ...recallOptions.factTypes && { fact_types: recallOptions.factTypes },
21689
+ ...recallOptions.memoryIds && { memory_ids: recallOptions.memoryIds }
21690
+ },
21691
+ GRAPH_LANE_ROWS
21692
+ );
21693
+ return rows.map(({ memory }) => ({
21694
+ memoryId: memory.memory_id,
21695
+ matched: [...canonicalTopics(memory)].filter((name) => wanted.has(name)).length
21696
+ })).filter((row) => row.matched > 0).sort((a, b) => b.matched - a.matched).slice(0, NODE_BUDGET).map((row) => row.memoryId);
21697
+ };
21486
21698
  const enumerateIds = async (read, ids, filters) => (await Promise.all(
21487
21699
  chunks2([...new Set(ids)], MAX_QUERY_IDS).map(
21488
21700
  (memory_ids) => pages(read, { ...filters, memory_ids })
@@ -21708,7 +21920,8 @@ function createRemoteMemoryStore(options) {
21708
21920
  };
21709
21921
  },
21710
21922
  extractTopics: async (memoryIds, extractOptions) => {
21711
- const extractedAt = extractOptions.now ?? Date.now();
21923
+ const start = Date.now();
21924
+ const extractedAt = extractOptions.now ?? start;
21712
21925
  const skippedIds = [];
21713
21926
  const skippedReasons = /* @__PURE__ */ new Map();
21714
21927
  const skip = (id, reason) => {
@@ -21726,7 +21939,18 @@ function createRemoteMemoryStore(options) {
21726
21939
  else if (memory.is_deleted || memory.topics_user_managed) skip(id, "excluded");
21727
21940
  else inputs.push({ id, content: memory.content });
21728
21941
  }
21729
- const entitiesByMemory = await extractEntitiesForMemories(inputs, extractOptions);
21942
+ const batches = [];
21943
+ const llmFailures = /* @__PURE__ */ new Map();
21944
+ const entitiesByMemory = await extractEntitiesForMemories(inputs, {
21945
+ ...extractOptions,
21946
+ onBatch: (batch) => {
21947
+ batches.push(batch);
21948
+ if (batch.failure) {
21949
+ for (const id of batch.memoryIds) llmFailures.set(id, batch.failure);
21950
+ }
21951
+ notifyMemoryCallback(extractOptions.onBatch, batch);
21952
+ }
21953
+ });
21730
21954
  const answered = /* @__PURE__ */ new Map();
21731
21955
  for (const input of inputs) {
21732
21956
  const entities = entitiesByMemory.get(input.id);
@@ -21775,7 +21999,16 @@ function createRemoteMemoryStore(options) {
21775
21999
  getLogger().warn("[memory/remote-store] Failed to write extracted topics:", error);
21776
22000
  for (const id of answered.keys()) fail(id, "link-failed");
21777
22001
  }
21778
- return { entitiesByMemory, stampedIds, skippedIds, skippedReasons };
22002
+ return {
22003
+ entitiesByMemory,
22004
+ stampedIds,
22005
+ skippedIds,
22006
+ skippedReasons,
22007
+ batchCount: batches.length,
22008
+ durationMs: Date.now() - start,
22009
+ batches,
22010
+ llmFailures
22011
+ };
21779
22012
  },
21780
22013
  stampTopicsExtracted: async (memoryIds, extractedAt, version = TOPICS_EXTRACTION_VERSION) => {
21781
22014
  const ids = [...new Set(memoryIds)];
@@ -21902,18 +22135,7 @@ function createRemoteMemoryStore(options) {
21902
22135
  listArchived: async () => (await enumerate({ archived_only: true, order: "archived", include_embeddings: true })).map(
21903
22136
  (record2) => stored(record2.memory)
21904
22137
  ),
21905
- memoriesByTopics: async (names) => {
21906
- const wanted = new Set(names.map(normalizeEntityName).filter(Boolean));
21907
- const out = /* @__PURE__ */ new Map();
21908
- const pages2 = await Promise.all(
21909
- chunks2([...wanted], MAX_QUERY_TOPICS).map((topics) => enumerate({ topics }))
21910
- );
21911
- for (const { memory } of pages2.flat()) {
21912
- const matched = [...canonicalTopics(memory)].filter((name) => wanted.has(name));
21913
- if (matched.length) out.set(memory.memory_id, new Set(matched));
21914
- }
21915
- return out;
21916
- },
22138
+ memoriesByTopics,
21917
22139
  topicsByMemories: async (memoryIds) => {
21918
22140
  const out = /* @__PURE__ */ new Map();
21919
22141
  for (const { memory } of await enumerateIds(decrypted, memoryIds, {
@@ -23791,9 +24013,13 @@ function formatVaultHits(hits) {
23791
24013
  return hits.map((h, i) => `[${i + 1}] (id: ${h.id}, similarity: ${h.score.toFixed(2)})
23792
24014
  ${h.content}`).join("\n\n");
23793
24015
  }
23794
- function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOptions) {
24016
+ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOptions, callbacks) {
23795
24017
  const limit = searchOptions?.limit ?? 5;
23796
24018
  const minSimilarity = searchOptions?.minSimilarity ?? 0.1;
24019
+ const recallEmbeddingOptions = {
24020
+ ...embeddingOptions,
24021
+ taskType: "memory_recall_embed"
24022
+ };
23797
24023
  const tuningForward = {
23798
24024
  ...searchOptions?.rerankTopN !== void 0 && { rerankTopN: searchOptions.rerankTopN },
23799
24025
  ...searchOptions?.ceWeight !== void 0 && { ceWeight: searchOptions.ceWeight },
@@ -23847,6 +24073,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
23847
24073
  const requestLimit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.floor(rawLimit), 1), RECALL_MAX_LIMIT) : limit;
23848
24074
  const argFolderId = args.folder_id;
23849
24075
  if (!query || typeof query !== "string") {
24076
+ notifyMemoryCallback(callbacks?.onError, new Error("A search query is required."));
23850
24077
  return "Error: A search query is required.";
23851
24078
  }
23852
24079
  try {
@@ -23854,7 +24081,11 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
23854
24081
  const budget = wantsDecompose ? "high" : searchOptions?.rerank ? "mid" : "low";
23855
24082
  const folderId = searchOptions?.folderId ?? argFolderId;
23856
24083
  if (searchOptions?.useFusion === false) {
24084
+ let candidateCount;
23857
24085
  const legacyOptions = {
24086
+ onCandidateCount: (count2) => {
24087
+ candidateCount = count2;
24088
+ },
23858
24089
  limit: requestLimit,
23859
24090
  minSimilarity,
23860
24091
  useFusion: false,
@@ -23862,13 +24093,38 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
23862
24093
  ...folderId !== void 0 && { folderId },
23863
24094
  ...searchOptions?.scopes && { scopes: searchOptions.scopes }
23864
24095
  };
23865
- const { results: legacy, embeddingsUnavailable } = "retain" in vaultCtx ? await vaultCtx.factSource.search(query, legacyOptions) : await searchVaultMemoriesWithSize(
24096
+ const start = Date.now();
24097
+ const outcome2 = "retain" in vaultCtx ? await vaultCtx.factSource.search(query, legacyOptions) : await searchVaultMemoriesWithSize(
23866
24098
  query,
23867
24099
  vaultCtx,
23868
- embeddingOptions,
24100
+ recallEmbeddingOptions,
23869
24101
  cache,
23870
24102
  legacyOptions
23871
24103
  );
24104
+ const { results: legacy, embeddingsUnavailable, vaultSize } = outcome2;
24105
+ const diagnostics = factRecallDiagnostics(
24106
+ legacy.map((m) => m.similarity),
24107
+ Date.now() - start,
24108
+ {
24109
+ vaultSize,
24110
+ minScore: minSimilarity,
24111
+ embeddingsUnavailable,
24112
+ candidateCount,
24113
+ truncated: candidateCount !== void 0 && candidateCount > requestLimit,
24114
+ decryptLast: outcome2.decryptLast,
24115
+ vaultRowsDecrypted: outcome2.rowsDecrypted,
24116
+ vaultRowsEmbedded: outcome2.rowsEmbedded,
24117
+ queryEmbedMs: outcome2.queryEmbedMs
24118
+ }
24119
+ );
24120
+ notifyMemoryCallback(callbacks?.onDiagnostics, diagnostics);
24121
+ if (diagnostics.truncated) {
24122
+ notifyMemoryCallback(callbacks?.onTruncated, {
24123
+ requestedLimit: requestLimit,
24124
+ effectiveLimit: requestLimit,
24125
+ truncatedByBudget: false
24126
+ });
24127
+ }
23872
24128
  if (legacy.length === 0) {
23873
24129
  return embeddingsUnavailable ? EMBEDDINGS_DEGRADED_EMPTY_NO_LEXICAL : "No relevant memories found in the vault.";
23874
24130
  }
@@ -23890,6 +24146,14 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
23890
24146
  {
23891
24147
  onDiagnostics: (d) => {
23892
24148
  recallDegraded = d.degraded;
24149
+ notifyMemoryCallback(callbacks?.onDiagnostics, d);
24150
+ if (d.truncated) {
24151
+ notifyMemoryCallback(callbacks?.onTruncated, {
24152
+ requestedLimit: requestLimit,
24153
+ effectiveLimit: requestLimit,
24154
+ truncatedByBudget: false
24155
+ });
24156
+ }
23893
24157
  },
23894
24158
  types: ["fact"],
23895
24159
  limit: requestLimit,
@@ -23922,6 +24186,10 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
23922
24186
 
23923
24187
  ${formatted}`;
23924
24188
  } catch (error) {
24189
+ notifyMemoryCallback(
24190
+ callbacks?.onError,
24191
+ error instanceof Error ? error : new Error(String(error))
24192
+ );
23925
24193
  const message = error instanceof Error ? error.message : "Unknown error";
23926
24194
  return `Error searching vault: ${message}`;
23927
24195
  }
@@ -25397,6 +25665,15 @@ function useChat(options) {
25397
25665
  detachControllerRef.current = null;
25398
25666
  pendingResumeRef.current = null;
25399
25667
  }, [resumable, fireCancel]);
25668
+ const cancel = (0, import_react4.useCallback)(
25669
+ (handle) => {
25670
+ if (resumable) fireCancel(handle);
25671
+ if (pendingResumeRef.current?.inferenceId === handle.inferenceId) {
25672
+ pendingResumeRef.current = null;
25673
+ }
25674
+ },
25675
+ [resumable, fireCancel]
25676
+ );
25400
25677
  const detach = (0, import_react4.useCallback)(() => {
25401
25678
  if (detachControllerRef.current) {
25402
25679
  detachControllerRef.current.abort();
@@ -25655,6 +25932,7 @@ function useChat(options) {
25655
25932
  isLoading,
25656
25933
  sendMessage,
25657
25934
  stop,
25935
+ cancel,
25658
25936
  detach,
25659
25937
  resumeStream: resumeStream2
25660
25938
  };
@@ -27064,6 +27342,7 @@ function useChatStorage(options) {
27064
27342
  isLoading,
27065
27343
  sendMessage: baseSendMessage,
27066
27344
  stop: baseStop,
27345
+ cancel: baseCancel,
27067
27346
  detach,
27068
27347
  resumeStream: baseResumeStream
27069
27348
  } = useChat({
@@ -27087,6 +27366,9 @@ function useChatStorage(options) {
27087
27366
  });
27088
27367
  const pendingResumeRef = (0, import_react5.useRef)(null);
27089
27368
  const isResumingRef = (0, import_react5.useRef)(false);
27369
+ const activeResumeRef = (0, import_react5.useRef)(null);
27370
+ const retirementRef = (0, import_react5.useRef)(null);
27371
+ const retirementStopRequestedRef = (0, import_react5.useRef)(false);
27090
27372
  const createConversation = (0, import_react5.useCallback)(
27091
27373
  async (opts) => {
27092
27374
  const { result, queued } = await writeOrQueue(
@@ -27286,6 +27568,143 @@ function useChatStorage(options) {
27286
27568
  activeToolSetsRef.current = activeToolSets;
27287
27569
  const getTokenRef = (0, import_react5.useRef)(getToken);
27288
27570
  getTokenRef.current = getToken;
27571
+ const finalizeResumedRow = (0, import_react5.useCallback)(
27572
+ async (ctx, data, wasStopped, responseDuration, viaQueue = false) => {
27573
+ const { content, thinking } = extractAssistantText(data);
27574
+ let parentMessageId = ctx.userMessageUniqueId;
27575
+ let knownIds = ctx.knownToolCallEventIds;
27576
+ if (ctx.convId && (!knownIds || !parentMessageId)) {
27577
+ let storedMessages = null;
27578
+ try {
27579
+ storedMessages = await getMessages(ctx.convId);
27580
+ } catch {
27581
+ }
27582
+ if (storedMessages) {
27583
+ if (!knownIds) {
27584
+ knownIds = /* @__PURE__ */ new Set();
27585
+ for (const msg of storedMessages) {
27586
+ if (msg.uniqueId === ctx.assistantUniqueId) continue;
27587
+ if (msg.toolCallEvents) {
27588
+ for (const evt of msg.toolCallEvents) {
27589
+ if (evt.id) knownIds.add(evt.id);
27590
+ }
27591
+ }
27592
+ }
27593
+ }
27594
+ if (!parentMessageId) {
27595
+ for (let i = storedMessages.length - 1; i >= 0; i--) {
27596
+ if (storedMessages[i].uniqueId !== ctx.assistantUniqueId) {
27597
+ parentMessageId = storedMessages[i].uniqueId;
27598
+ break;
27599
+ }
27600
+ }
27601
+ }
27602
+ }
27603
+ }
27604
+ const allToolCallEvents = getToolCallEvents(data);
27605
+ const currentTurnToolCallEvents = knownIds ? allToolCallEvents?.filter(
27606
+ (evt) => evt.id !== void 0 && evt.id !== null && !knownIds.has(evt.id)
27607
+ ) : allToolCallEvents;
27608
+ const baseSources = ctx.sources ?? [];
27609
+ const seenSourceUrls = new Set(
27610
+ baseSources.map((s) => s.url).filter((url) => !!url)
27611
+ );
27612
+ const toolEventSources = extractSourcesFromToolCallEvents(currentTurnToolCallEvents);
27613
+ const sources = [
27614
+ ...baseSources,
27615
+ ...toolEventSources.filter((s) => !s.url || !seenSourceUrls.has(s.url))
27616
+ ].filter((source) => !source.url?.includes(MCP_R2_DOMAIN));
27617
+ const rowOpts = {
27618
+ conversationId: ctx.convId,
27619
+ role: "assistant",
27620
+ content,
27621
+ model: data.model || ctx.model || "",
27622
+ imageModel: ctx.imageModel || getImageModel(data),
27623
+ usage: convertUsageToStored(data),
27624
+ responseDuration,
27625
+ sources,
27626
+ thoughtProcess: finalizeThoughtProcess(ctx.thoughtProcess),
27627
+ thinking,
27628
+ wasStopped,
27629
+ parentMessageId,
27630
+ toolCallEvents: currentTurnToolCallEvents && currentTurnToolCallEvents.length > 0 ? currentTurnToolCallEvents : void 0,
27631
+ uniqueId: ctx.assistantUniqueId
27632
+ };
27633
+ if (!viaQueue) return upsertMessageOp(storageCtx, rowOpts);
27634
+ const { result } = await writeOrQueue(
27635
+ "createMessage",
27636
+ rowOpts,
27637
+ () => upsertMessageOp(storageCtx, rowOpts),
27638
+ () => makeSyntheticStoredMessage(rowOpts),
27639
+ ctx.userMessageQueueId ? [ctx.userMessageQueueId] : []
27640
+ );
27641
+ return result;
27642
+ },
27643
+ [storageCtx, getMessages, writeOrQueue]
27644
+ );
27645
+ const retirePendingTurn = (0, import_react5.useCallback)(async () => {
27646
+ const visible = activeResumeRef.current;
27647
+ if (visible && !visible.headless) {
27648
+ baseStop();
27649
+ await visible.settled;
27650
+ }
27651
+ if (retirementRef.current) return retirementRef.current;
27652
+ const active = activeResumeRef.current;
27653
+ if (active && !active.headless) return "Resume already in progress";
27654
+ const pending = pendingResumeRef.current;
27655
+ if (!pending) return null;
27656
+ if (!pending.userMessageUniqueId) {
27657
+ if (active?.context === pending) return null;
27658
+ if (!pending.partialData) {
27659
+ pendingResumeRef.current = null;
27660
+ return null;
27661
+ }
27662
+ }
27663
+ if (active?.context === pending) return "Resume already in progress";
27664
+ pendingResumeRef.current = null;
27665
+ retirementStopRequestedRef.current = false;
27666
+ const retirement = Promise.resolve().then(async () => {
27667
+ try {
27668
+ const data = pending.partialData;
27669
+ if (data && pending.assistantUniqueId) {
27670
+ const { content, thinking } = extractAssistantText(data);
27671
+ if (content || thinking || (getToolCallEvents(data)?.length ?? 0) > 0) {
27672
+ if (walletAddress && signMessage && !hasEncryptionKey(walletAddress)) {
27673
+ try {
27674
+ await requestEncryptionKey(walletAddress, signMessage, embeddedWalletSigner);
27675
+ } catch {
27676
+ }
27677
+ }
27678
+ await finalizeResumedRow(
27679
+ pending,
27680
+ data,
27681
+ true,
27682
+ (Date.now() - pending.startTime) / 1e3,
27683
+ true
27684
+ );
27685
+ }
27686
+ }
27687
+ if (pending.handle) baseCancel(pending.handle);
27688
+ return null;
27689
+ } catch (err) {
27690
+ pendingResumeRef.current = pending;
27691
+ if (retirementStopRequestedRef.current && pending.handle) {
27692
+ baseCancel(pending.handle);
27693
+ pendingResumeRef.current = { ...pending, handle: null };
27694
+ }
27695
+ return err instanceof Error ? err.message : "Failed to store detached message";
27696
+ }
27697
+ });
27698
+ retirementRef.current = retirement;
27699
+ try {
27700
+ return await retirement;
27701
+ } finally {
27702
+ if (retirementRef.current === retirement) {
27703
+ retirementRef.current = null;
27704
+ retirementStopRequestedRef.current = false;
27705
+ }
27706
+ }
27707
+ }, [baseCancel, baseStop, finalizeResumedRow, walletAddress, signMessage, embeddedWalletSigner]);
27289
27708
  const sendMessage = (0, import_react5.useCallback)(
27290
27709
  async (args) => {
27291
27710
  const {
@@ -27337,7 +27756,11 @@ function useChatStorage(options) {
27337
27756
  maskForCall: (text14) => redactor ? redactor.maskText(text14) : text14
27338
27757
  };
27339
27758
  };
27340
- pendingResumeRef.current = null;
27759
+ const retirementError = await retirePendingTurn();
27760
+ if (retirementError) {
27761
+ if (!skipStorage) return { data: null, error: retirementError };
27762
+ getLogger().warn("[useChatStorage] detached partial not saved:", retirementError);
27763
+ }
27341
27764
  const effectiveAssistantUniqueId = assistantUniqueId ?? (resumable ? `msg_${(0, import_uuid7.v7)()}` : void 0);
27342
27765
  const embedToolText = (text14, mask, masked, token) => {
27343
27766
  const opts = {
@@ -27861,6 +28284,7 @@ function useChatStorage(options) {
27861
28284
  handle: detachedResult.resume ?? null,
27862
28285
  convId,
27863
28286
  userMessageUniqueId: storedUserMessage.uniqueId,
28287
+ userMessageQueueId: userMsgQueueId,
27864
28288
  assistantUniqueId: rowId,
27865
28289
  model,
27866
28290
  imageModel,
@@ -28087,6 +28511,7 @@ function useChatStorage(options) {
28087
28511
  getMessages,
28088
28512
  storageCtx,
28089
28513
  baseSendMessage,
28514
+ retirePendingTurn,
28090
28515
  embedMessageAsync,
28091
28516
  minContentLength,
28092
28517
  walletAddress,
@@ -28101,72 +28526,9 @@ function useChatStorage(options) {
28101
28526
  onPiiRedacted
28102
28527
  ]
28103
28528
  );
28104
- const finalizeResumedRow = (0, import_react5.useCallback)(
28105
- async (ctx, data, wasStopped, responseDuration) => {
28106
- const { content, thinking } = extractAssistantText(data);
28107
- let parentMessageId = ctx.userMessageUniqueId;
28108
- let knownIds = ctx.knownToolCallEventIds;
28109
- if (ctx.convId && (!knownIds || !parentMessageId)) {
28110
- let storedMessages = null;
28111
- try {
28112
- storedMessages = await getMessages(ctx.convId);
28113
- } catch {
28114
- }
28115
- if (storedMessages) {
28116
- if (!knownIds) {
28117
- knownIds = /* @__PURE__ */ new Set();
28118
- for (const msg of storedMessages) {
28119
- if (msg.toolCallEvents) {
28120
- for (const evt of msg.toolCallEvents) {
28121
- if (evt.id) knownIds.add(evt.id);
28122
- }
28123
- }
28124
- }
28125
- }
28126
- if (!parentMessageId) {
28127
- for (let i = storedMessages.length - 1; i >= 0; i--) {
28128
- if (storedMessages[i].uniqueId !== ctx.assistantUniqueId) {
28129
- parentMessageId = storedMessages[i].uniqueId;
28130
- break;
28131
- }
28132
- }
28133
- }
28134
- }
28135
- }
28136
- const allToolCallEvents = getToolCallEvents(data);
28137
- const currentTurnToolCallEvents = knownIds ? allToolCallEvents?.filter(
28138
- (evt) => evt.id !== void 0 && evt.id !== null && !knownIds.has(evt.id)
28139
- ) : allToolCallEvents;
28140
- const baseSources = ctx.sources ?? [];
28141
- const seenSourceUrls = new Set(
28142
- baseSources.map((s) => s.url).filter((url) => !!url)
28143
- );
28144
- const toolEventSources = extractSourcesFromToolCallEvents(currentTurnToolCallEvents);
28145
- const sources = [
28146
- ...baseSources,
28147
- ...toolEventSources.filter((s) => !s.url || !seenSourceUrls.has(s.url))
28148
- ].filter((source) => !source.url?.includes(MCP_R2_DOMAIN));
28149
- return upsertMessageOp(storageCtx, {
28150
- conversationId: ctx.convId,
28151
- role: "assistant",
28152
- content,
28153
- model: data.model || ctx.model || "",
28154
- imageModel: ctx.imageModel || getImageModel(data),
28155
- usage: convertUsageToStored(data),
28156
- responseDuration,
28157
- sources,
28158
- thoughtProcess: finalizeThoughtProcess(ctx.thoughtProcess),
28159
- thinking,
28160
- wasStopped,
28161
- parentMessageId,
28162
- uniqueId: ctx.assistantUniqueId
28163
- });
28164
- },
28165
- [storageCtx, getMessages]
28166
- );
28167
28529
  const resumeStream2 = (0, import_react5.useCallback)(
28168
28530
  async (handleOverride, opts) => {
28169
- if (isResumingRef.current) {
28531
+ if (isResumingRef.current || retirementRef.current) {
28170
28532
  return { data: null, error: "Resume already in progress", assistantMessage: null };
28171
28533
  }
28172
28534
  const pending = pendingResumeRef.current;
@@ -28195,6 +28557,7 @@ function useChatStorage(options) {
28195
28557
  if (pendingResumeRef.current === rctx) pendingResumeRef.current = null;
28196
28558
  };
28197
28559
  const safeFinalize = async (data, wasStopped, responseDuration) => {
28560
+ rctx.partialData = data;
28198
28561
  try {
28199
28562
  return { message: await finalizeResumedRow(rctx, data, wasStopped, responseDuration) };
28200
28563
  } catch (writeErr) {
@@ -28204,6 +28567,13 @@ function useChatStorage(options) {
28204
28567
  }
28205
28568
  };
28206
28569
  isResumingRef.current = true;
28570
+ let markSettled = () => {
28571
+ };
28572
+ const settled = new Promise((resolve) => {
28573
+ markSettled = resolve;
28574
+ });
28575
+ const activeResume = { context: rctx, headless: opts?.headless === true, settled };
28576
+ activeResumeRef.current = activeResume;
28207
28577
  try {
28208
28578
  const result = await baseResumeStream(handle, { headless: opts?.headless });
28209
28579
  const responseDuration = (Date.now() - rctx.startTime) / 1e3;
@@ -28280,11 +28650,17 @@ function useChatStorage(options) {
28280
28650
  throw err;
28281
28651
  } finally {
28282
28652
  isResumingRef.current = false;
28653
+ if (activeResumeRef.current === activeResume) activeResumeRef.current = null;
28654
+ markSettled();
28283
28655
  }
28284
28656
  },
28285
28657
  [baseResumeStream, currentConversationId, finalizeResumedRow, embedMessageAsync]
28286
28658
  );
28287
28659
  const stop = (0, import_react5.useCallback)(() => {
28660
+ if (retirementRef.current) {
28661
+ retirementStopRequestedRef.current = true;
28662
+ return;
28663
+ }
28288
28664
  const pending = pendingResumeRef.current;
28289
28665
  if (pending && pending.assistantUniqueId && !isResumingRef.current) {
28290
28666
  const finalize = pending;