@anuma/sdk 6.25.1 → 6.25.3

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.
@@ -9809,8 +9809,42 @@ async function vaultMemoryToStored(memory, walletAddress, signMessage, embeddedW
9809
9809
  }
9810
9810
  return raw;
9811
9811
  }
9812
+ function populateNewVaultMemory(record, ctx, opts, encryptedContent) {
9813
+ record._setRaw("content", encryptedContent);
9814
+ record._setRaw("scope", opts.scope ?? "private");
9815
+ record._setRaw("folder_id", opts.folderId ?? null);
9816
+ record._setRaw("user_id", ctx.userId ?? null);
9817
+ record._setRaw("is_deleted", false);
9818
+ if (opts.embedding !== void 0) {
9819
+ record._setRaw("embedding", opts.embedding);
9820
+ record._setRaw("embedding_model", opts.embeddingModel ?? null);
9821
+ }
9822
+ if (opts.sourceChunkIds !== void 0) {
9823
+ record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
9824
+ }
9825
+ record._setRaw("proof_count", opts.proofCount ?? 1);
9826
+ record._setRaw("source", opts.source ?? "manual");
9827
+ if (opts.eventTime) {
9828
+ record._setRaw("event_time_start", opts.eventTime.start ?? null);
9829
+ record._setRaw("event_time_end", opts.eventTime.end ?? null);
9830
+ record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
9831
+ }
9832
+ if (opts.factType !== void 0) {
9833
+ record._setRaw("fact_type", opts.factType);
9834
+ }
9835
+ if (opts.trustTier !== void 0) {
9836
+ record._setRaw("trust_tier", normalizeTrustTier(opts.trustTier));
9837
+ }
9838
+ record._setRaw("visibility", opts.visibility ?? "private");
9839
+ record._setRaw(
9840
+ "published_at",
9841
+ opts.visibility && opts.visibility !== "private" ? opts.publishedAt ?? Date.now() : null
9842
+ );
9843
+ if (opts.geohash !== void 0) {
9844
+ record._setRaw("geohash", opts.geohash);
9845
+ }
9846
+ }
9812
9847
  async function createVaultMemoryOp(ctx, opts) {
9813
- const scope = opts.scope ?? "private";
9814
9848
  const encryptedContent = ctx.walletAddress && ctx.signMessage ? await encryptVaultMemoryContent(
9815
9849
  opts.content,
9816
9850
  ctx.walletAddress,
@@ -9820,47 +9854,14 @@ async function createVaultMemoryOp(ctx, opts) {
9820
9854
  const created = await ctx.database.write(async () => {
9821
9855
  if (ctx.canWrite && !await ctx.canWrite())
9822
9856
  throw new Error("Memory source is no longer eligible");
9823
- return ctx.vaultMemoryCollection.create((record) => {
9824
- record._setRaw("content", encryptedContent);
9825
- record._setRaw("scope", scope);
9826
- record._setRaw("folder_id", opts.folderId ?? null);
9827
- record._setRaw("user_id", ctx.userId ?? null);
9828
- record._setRaw("is_deleted", false);
9829
- if (opts.embedding !== void 0) {
9830
- record._setRaw("embedding", opts.embedding);
9831
- record._setRaw("embedding_model", opts.embeddingModel ?? null);
9832
- }
9833
- if (opts.sourceChunkIds !== void 0) {
9834
- record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
9835
- }
9836
- record._setRaw("proof_count", opts.proofCount ?? 1);
9837
- record._setRaw("source", opts.source ?? "manual");
9838
- if (opts.eventTime) {
9839
- record._setRaw("event_time_start", opts.eventTime.start ?? null);
9840
- record._setRaw("event_time_end", opts.eventTime.end ?? null);
9841
- record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
9842
- }
9843
- if (opts.factType !== void 0) {
9844
- record._setRaw("fact_type", opts.factType);
9845
- }
9846
- if (opts.trustTier !== void 0) {
9847
- record._setRaw("trust_tier", normalizeTrustTier(opts.trustTier));
9848
- }
9849
- record._setRaw("visibility", opts.visibility ?? "private");
9850
- record._setRaw(
9851
- "published_at",
9852
- opts.visibility && opts.visibility !== "private" ? opts.publishedAt ?? Date.now() : null
9853
- );
9854
- if (opts.geohash !== void 0) {
9855
- record._setRaw("geohash", opts.geohash);
9856
- }
9857
- });
9857
+ return ctx.vaultMemoryCollection.create(
9858
+ (record) => populateNewVaultMemory(record, ctx, opts, encryptedContent)
9859
+ );
9858
9860
  });
9859
9861
  return vaultMemoryToStored(created, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner);
9860
9862
  }
9861
9863
  async function createSupersedingMemoryOp(ctx, opts, targetId) {
9862
9864
  if (!targetId) return { created: null, retired: false };
9863
- const scope = opts.scope ?? "private";
9864
9865
  const encryptedContent = ctx.walletAddress && ctx.signMessage ? await encryptVaultMemoryContent(
9865
9866
  opts.content,
9866
9867
  ctx.walletAddress,
@@ -9878,27 +9879,9 @@ async function createSupersedingMemoryOp(ctx, opts, targetId) {
9878
9879
  return;
9879
9880
  }
9880
9881
  if (target.isDeleted || target.supersededBy || !isOwnedByCtxUser(ctx, target)) return;
9881
- createdRecord = await ctx.vaultMemoryCollection.create((record) => {
9882
- record._setRaw("content", encryptedContent);
9883
- record._setRaw("scope", scope);
9884
- record._setRaw("folder_id", opts.folderId ?? null);
9885
- record._setRaw("user_id", ctx.userId ?? null);
9886
- record._setRaw("is_deleted", false);
9887
- if (opts.embedding !== void 0) {
9888
- record._setRaw("embedding", opts.embedding);
9889
- record._setRaw("embedding_model", opts.embeddingModel ?? null);
9890
- }
9891
- if (opts.sourceChunkIds !== void 0) {
9892
- record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
9893
- }
9894
- record._setRaw("proof_count", opts.proofCount ?? 1);
9895
- record._setRaw("source", opts.source ?? "manual");
9896
- if (opts.eventTime) {
9897
- record._setRaw("event_time_start", opts.eventTime.start ?? null);
9898
- record._setRaw("event_time_end", opts.eventTime.end ?? null);
9899
- record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
9900
- }
9901
- });
9882
+ createdRecord = await ctx.vaultMemoryCollection.create(
9883
+ (record) => populateNewVaultMemory(record, ctx, opts, encryptedContent)
9884
+ );
9902
9885
  await target.update((r) => {
9903
9886
  r._setRaw("superseded_by", createdRecord.id);
9904
9887
  r._setRaw("superseded_at", Date.now());
@@ -9978,41 +9961,9 @@ async function createVaultMemoriesBatchOp(ctx, optionsArray) {
9978
9961
  if (ctx.canWrite && !await ctx.canWrite())
9979
9962
  throw new Error("Memory source is no longer eligible");
9980
9963
  const prepared = optionsArray.map(
9981
- (opts, i) => ctx.vaultMemoryCollection.prepareCreate((record) => {
9982
- record._setRaw("content", encryptedContents[i]);
9983
- record._setRaw("scope", opts.scope ?? "private");
9984
- record._setRaw("folder_id", opts.folderId ?? null);
9985
- record._setRaw("user_id", ctx.userId ?? null);
9986
- record._setRaw("is_deleted", false);
9987
- if (optionsArray[i].embedding !== void 0) {
9988
- record._setRaw("embedding", optionsArray[i].embedding);
9989
- record._setRaw("embedding_model", optionsArray[i].embeddingModel ?? null);
9990
- }
9991
- if (opts.sourceChunkIds !== void 0) {
9992
- record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
9993
- }
9994
- record._setRaw("proof_count", opts.proofCount ?? 1);
9995
- record._setRaw("source", opts.source ?? "manual");
9996
- if (opts.eventTime) {
9997
- record._setRaw("event_time_start", opts.eventTime.start ?? null);
9998
- record._setRaw("event_time_end", opts.eventTime.end ?? null);
9999
- record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
10000
- }
10001
- if (opts.factType !== void 0) {
10002
- record._setRaw("fact_type", opts.factType);
10003
- }
10004
- if (opts.trustTier !== void 0) {
10005
- record._setRaw("trust_tier", normalizeTrustTier(opts.trustTier));
10006
- }
10007
- record._setRaw("visibility", opts.visibility ?? "private");
10008
- record._setRaw(
10009
- "published_at",
10010
- opts.visibility && opts.visibility !== "private" ? opts.publishedAt ?? Date.now() : null
10011
- );
10012
- if (opts.geohash !== void 0) {
10013
- record._setRaw("geohash", opts.geohash);
10014
- }
10015
- })
9964
+ (opts, i) => ctx.vaultMemoryCollection.prepareCreate(
9965
+ (record) => populateNewVaultMemory(record, ctx, opts, encryptedContents[i])
9966
+ )
10016
9967
  );
10017
9968
  await ctx.database.batch(...prepared);
10018
9969
  return prepared;
@@ -10833,21 +10784,73 @@ async function hardDeleteDecayedOp(ctx, id, opts) {
10833
10784
  return false;
10834
10785
  }
10835
10786
  }
10836
- async function updateVaultMemoryEmbeddingOp(ctx, id, embedding, embeddingModel) {
10787
+ async function updateVaultMemoryEmbeddingOp(ctx, id, embedding, embeddingModel, expected) {
10837
10788
  try {
10838
10789
  const record = await ctx.vaultMemoryCollection.find(id);
10839
10790
  if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) return false;
10791
+ const readUpdatedAt = record.updatedAt.getTime();
10792
+ const readStoredContent = record._getRaw("content");
10793
+ if (expected?.updatedAt !== void 0 && expected.updatedAt !== readUpdatedAt) return false;
10794
+ if (expected?.content !== void 0) {
10795
+ const current = await vaultMemoryToStored(
10796
+ record,
10797
+ ctx.walletAddress,
10798
+ ctx.signMessage,
10799
+ ctx.embeddedWalletSigner
10800
+ );
10801
+ if (current.content !== expected.content) return false;
10802
+ }
10803
+ let written = false;
10840
10804
  await ctx.database.write(async () => {
10805
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) return;
10806
+ const originalUpdatedAt = record.updatedAt.getTime();
10807
+ if (originalUpdatedAt !== readUpdatedAt || record._getRaw("content") !== readStoredContent)
10808
+ return;
10841
10809
  await record.update((r) => {
10842
10810
  r._setRaw("embedding", embedding);
10843
10811
  r._setRaw("embedding_model", embeddingModel);
10812
+ r._setRaw("updated_at", originalUpdatedAt);
10844
10813
  });
10814
+ written = true;
10845
10815
  });
10846
- return true;
10816
+ return written;
10847
10817
  } catch {
10848
10818
  return false;
10849
10819
  }
10850
10820
  }
10821
+ function normalizeForDedupe(content) {
10822
+ return content.normalize("NFKC").trim().replace(/\s+/g, " ").toLowerCase();
10823
+ }
10824
+ async function findQuarantinedDuplicateOp(ctx, content, sourceIds, where) {
10825
+ const wanted = [...new Set(sourceIds)].sort().join("\0");
10826
+ const target = normalizeForDedupe(content);
10827
+ const rows = await ctx.vaultMemoryCollection.query(
10828
+ Q5.where("trust_tier", "quarantined"),
10829
+ Q5.where("is_deleted", false),
10830
+ Q5.where("scope", where.scope),
10831
+ Q5.where("folder_id", where.folderId),
10832
+ ...ctx.userId !== void 0 ? [Q5.where("user_id", ctx.userId)] : []
10833
+ ).unsafeFetchRaw();
10834
+ for (const raw of rows) {
10835
+ let ids;
10836
+ try {
10837
+ ids = JSON.parse(raw.source_chunk_ids ?? "[]");
10838
+ } catch {
10839
+ continue;
10840
+ }
10841
+ if (!Array.isArray(ids)) continue;
10842
+ const key = [...new Set(ids.filter((id) => typeof id === "string"))].sort().join("\0");
10843
+ if (key !== wanted) continue;
10844
+ const stored = await vaultMemoryRawToStored(
10845
+ raw,
10846
+ ctx.walletAddress,
10847
+ ctx.signMessage,
10848
+ ctx.embeddedWalletSigner
10849
+ );
10850
+ if (normalizeForDedupe(stored.content) === target) return stored.uniqueId;
10851
+ }
10852
+ return null;
10853
+ }
10851
10854
 
10852
10855
  // src/lib/memory/decay.ts
10853
10856
  var DAY_MS = 24 * 60 * 60 * 1e3;
@@ -10871,6 +10874,10 @@ var DEFAULT_DECAY_POLICY = {
10871
10874
  hardDeleteWindowMs: HARD_DELETE_WINDOW_MS,
10872
10875
  pastEventGraceMs: PAST_EVENT_GRACE_MS
10873
10876
  };
10877
+ function lastActivityAt(m) {
10878
+ const observedAt = Number.isFinite(m.lastObservedAt) ? m.lastObservedAt : m.updatedAt;
10879
+ return Math.max(m.updatedAt, observedAt);
10880
+ }
10874
10881
  function resolvePolicy(policy) {
10875
10882
  if (!policy) return DEFAULT_DECAY_POLICY;
10876
10883
  return {
@@ -10910,8 +10917,7 @@ function classifyDecay(m, now, policy) {
10910
10917
  if (m.eventTimeEnd < now - resolved.pastEventGraceMs) return "archive";
10911
10918
  if (m.eventTimeEnd >= now) return "keep";
10912
10919
  }
10913
- const observedAt = Number.isFinite(m.lastObservedAt) ? m.lastObservedAt : m.updatedAt;
10914
- if (now - Math.max(m.updatedAt, observedAt) > ttlForType(m.factType, resolved)) return "archive";
10920
+ if (now - lastActivityAt(m) > ttlForType(m.factType, resolved)) return "archive";
10915
10921
  return "keep";
10916
10922
  }
10917
10923
 
@@ -14251,6 +14257,24 @@ async function extractAndRetain(messages, retainCtx, options) {
14251
14257
  const tRetain = Date.now();
14252
14258
  for (const { candidate, isQuarantined, reason, signature } of toRetain) {
14253
14259
  try {
14260
+ if (isQuarantined) {
14261
+ const existingId = await findQuarantinedDuplicateOp(
14262
+ retainCtx.vaultCtx,
14263
+ candidate.content,
14264
+ candidate.sourceMessageIds,
14265
+ // The same defaults retain() writes the row with.
14266
+ { scope: options.scope ?? "private", folderId: options.folderId ?? null }
14267
+ ).catch(() => null);
14268
+ if (existingId) {
14269
+ quarantinedInfo.push({
14270
+ candidate,
14271
+ memoryId: existingId,
14272
+ reason,
14273
+ signature
14274
+ });
14275
+ continue;
14276
+ }
14277
+ }
14254
14278
  const result = await retain(candidate.content, retainCtx, {
14255
14279
  source: "auto-extracted",
14256
14280
  sourceChunkIds: candidate.sourceMessageIds,
@@ -14696,7 +14720,7 @@ function createLlmDecayClassifier(options) {
14696
14720
  if (!content || content.trim().length === 0) return ruleVerdict;
14697
14721
  const trimmed = content.trim().slice(0, MAX_CONTENT_CHARS);
14698
14722
  const safe = redactor ? (await redactor.redactTextAsync(trimmed)).text : trimmed;
14699
- const meta = `factType: ${input.factType ?? "none"}; ageDays: ${ageDays(input.updatedAt, now)}`;
14723
+ const meta = `factType: ${input.factType ?? "none"}; ageDays: ${ageDays(lastActivityAt(input), now)}`;
14700
14724
  let parsed;
14701
14725
  try {
14702
14726
  parsed = await callPortalJsonCompletion({
@@ -14729,9 +14753,9 @@ Should this be kept or archived?`,
14729
14753
  }
14730
14754
  };
14731
14755
  }
14732
- function ageDays(updatedAt, now) {
14733
- if (!Number.isFinite(updatedAt) || !Number.isFinite(now)) return 0;
14734
- return Math.max(0, Math.floor((now - updatedAt) / (24 * 60 * 60 * 1e3)));
14756
+ function ageDays(activityAt, now) {
14757
+ if (!Number.isFinite(activityAt) || !Number.isFinite(now)) return 0;
14758
+ return Math.max(0, Math.floor((now - activityAt) / (24 * 60 * 60 * 1e3)));
14735
14759
  }
14736
14760
  function parseVerdict(parsed) {
14737
14761
  if (typeof parsed !== "object" || parsed === null) return null;
@@ -14784,7 +14808,7 @@ function createDecaySweeper(options) {
14784
14808
  if (!classifier || !isBorderline(input)) return ruleVerdict;
14785
14809
  if (input.id) {
14786
14810
  const cached = classifierCache.get(input.id);
14787
- if (cached && cached.updatedAt === input.updatedAt) return cached.verdict;
14811
+ if (cached && cached.activityAt === lastActivityAt(input)) return cached.verdict;
14788
14812
  }
14789
14813
  if (sweep.classifierCalls >= maxClassifierCalls) {
14790
14814
  if (!sweep.ceilingLogged) {
@@ -14798,7 +14822,7 @@ function createDecaySweeper(options) {
14798
14822
  sweep.classifierCalls++;
14799
14823
  try {
14800
14824
  const verdict = await classifier.classify(input, ruleVerdict, now);
14801
- if (input.id) classifierCache.set(input.id, { updatedAt: input.updatedAt, verdict });
14825
+ if (input.id) classifierCache.set(input.id, { activityAt: lastActivityAt(input), verdict });
14802
14826
  return verdict;
14803
14827
  } catch (err) {
14804
14828
  getLogger().warn(
@@ -16525,7 +16549,10 @@ function reviewedMemoryIdsSignature(ids) {
16525
16549
  return [...new Set(ids)].sort().join("\n");
16526
16550
  }
16527
16551
  function facetsSignature(facets) {
16528
- return facets.map((f) => JSON.stringify([f.key, f.label, f.query, f.guidance, facetResponseSchema(f.key)])).sort().join("\n");
16552
+ return [
16553
+ JSON.stringify(FACET_SYSTEM_PROMPT),
16554
+ ...facets.map((f) => JSON.stringify([f.key, f.label, f.query, f.guidance, facetResponseSchema(f.key)])).sort()
16555
+ ].join("\n");
16529
16556
  }
16530
16557
  function trendsEqual(a, b) {
16531
16558
  if (!a) return false;
@@ -16747,7 +16774,7 @@ var FACET_SYSTEM_PROMPT = `You are writing one section of a person's shareable p
16747
16774
  Rules:
16748
16775
  - Ground every claim in the supplied memories \u2014 never invent, infer beyond, or embellish what they support.
16749
16776
  - If the memories don't cover this section, return an empty summary with hasEvidence=false. Do not pad or guess.
16750
- - Write in third person about the person, in a natural voice suitable for a public profile (no "I"/"you", no name repetition).
16777
+ - Write in a pronoun-free profile voice: lead with the descriptor or the verb ("Thoughtful, detail-oriented builder. Values clear communication."). Never use "they"/"he"/"she", "I", "you", or the person's name \u2014 the person publishes this as their own profile, and strangers read it there.
16751
16778
  - Be concise and specific; no preamble, hedging, or meta-commentary.
16752
16779
  - Respond as JSON in exactly the shape the user turn states, with no extra fields.`;
16753
16780
  function buildFacetUserInstructions(facet) {
@@ -17416,6 +17443,37 @@ async function assembleMemoryContext(options) {
17416
17443
  import { Q as Q7 } from "@nozbe/watermelondb";
17417
17444
  var MAX_JOBS_PER_PASS = 3;
17418
17445
  var MAX_ATTEMPTS = 3;
17446
+ var MAX_FAILED_SESSIONS = 3;
17447
+ var MIN_FAILED_SESSION_GAP_MS = 60 * 60 * 1e3;
17448
+ var REQUEST_REJECTED_STATUSES = /* @__PURE__ */ new Set([400, 404, 413, 422]);
17449
+ var CONTENT_FAILURE_REASONS = /* @__PURE__ */ new Set([
17450
+ "empty-content",
17451
+ "invalid-json",
17452
+ "null-completion",
17453
+ "body-parse-failed"
17454
+ ]);
17455
+ var BACKOFF_ALLOWANCE_MS = 2100;
17456
+ var RETAIN_BUDGET_MS = 12e4;
17457
+ function defaultBatchTimeoutMs(extract) {
17458
+ const attempts = Math.max(1, extract.maxAttempts ?? 3);
17459
+ const calls = (extract.timeoutMs ?? 6e4) * attempts + BACKOFF_ALLOWANCE_MS * (attempts - 1);
17460
+ const extraction = extract.totalTimeoutMs !== void 0 ? Math.min(extract.totalTimeoutMs, calls) : calls;
17461
+ return extraction + RETAIN_BUDGET_MS;
17462
+ }
17463
+ var BatchFailureError = class extends Error {
17464
+ constructor(message, terminal) {
17465
+ super(message);
17466
+ this.terminal = terminal;
17467
+ }
17468
+ };
17469
+ var AccountFailureError = class extends Error {
17470
+ };
17471
+ var LockedSourcesError = class extends Error {
17472
+ constructor(persistentIds) {
17473
+ super("Source messages are locked; retained for retry");
17474
+ this.persistentIds = persistentIds;
17475
+ }
17476
+ };
17419
17477
  function idsOf(job) {
17420
17478
  return JSON.parse(String(job._getRaw("message_ids")));
17421
17479
  }
@@ -17423,6 +17481,16 @@ function seqOf(job) {
17423
17481
  const raw = job._getRaw("watermark_seq");
17424
17482
  return typeof raw === "number" && raw > 0 ? raw : void 0;
17425
17483
  }
17484
+ function failedSessionsOf(job, head) {
17485
+ if (job._getRaw("failed_head") !== head) return 0;
17486
+ const raw = job._getRaw("failed_sessions");
17487
+ return typeof raw === "number" && raw > 0 ? raw : 0;
17488
+ }
17489
+ function failedAtOf(job, head) {
17490
+ if (job._getRaw("failed_head") !== head) return void 0;
17491
+ const raw = job._getRaw("failed_at");
17492
+ return typeof raw === "number" ? raw : void 0;
17493
+ }
17426
17494
  function createDurableAutoExtractor(options) {
17427
17495
  const vault = options.retainCtx.vaultCtx;
17428
17496
  const database = vault.database;
@@ -17445,6 +17513,9 @@ function createDurableAutoExtractor(options) {
17445
17513
  let writes = Promise.resolve();
17446
17514
  let storeFailures = 0;
17447
17515
  const attempts = /* @__PURE__ */ new Map();
17516
+ const sessionFailures = /* @__PURE__ */ new Map();
17517
+ const skippedThisSession = /* @__PURE__ */ new Set();
17518
+ const clock = options.now ?? Date.now;
17448
17519
  const reportError = (error, conversationId) => {
17449
17520
  try {
17450
17521
  options.onError?.(error instanceof Error ? error : new Error(String(error)), conversationId);
@@ -17473,6 +17544,7 @@ function createDurableAutoExtractor(options) {
17473
17544
  }
17474
17545
  async function extractBatch(messages, conversationId, ids, jobScope, jobFolderId) {
17475
17546
  let watchdog;
17547
+ let cancelled = false;
17476
17548
  const model = options.modelForScope?.(jobScope) ?? options.extract.model;
17477
17549
  try {
17478
17550
  return await Promise.race([
@@ -17483,7 +17555,7 @@ function createDurableAutoExtractor(options) {
17483
17555
  ...options.retainCtx,
17484
17556
  vaultCtx: {
17485
17557
  ...vault,
17486
- canWrite: async () => !disposed && await storage.conversationsCollection.query(Q7.where("conversation_id", conversationId), Q7.where("is_deleted", false)).fetchCount() > 0 && await storage.messagesCollection.query(Q7.where("conversation_id", conversationId), Q7.where("id", Q7.oneOf(ids))).fetchCount() === ids.length && (!vault.canWrite || await vault.canWrite()) && !disposed
17558
+ canWrite: async () => !disposed && !cancelled && await storage.conversationsCollection.query(Q7.where("conversation_id", conversationId), Q7.where("is_deleted", false)).fetchCount() > 0 && await storage.messagesCollection.query(Q7.where("conversation_id", conversationId), Q7.where("id", Q7.oneOf(ids))).fetchCount() === ids.length && (!vault.canWrite || await vault.canWrite()) && !disposed && !cancelled
17487
17559
  }
17488
17560
  },
17489
17561
  // The model follows the job's scope, not the mode alive at drain time.
@@ -17503,14 +17575,116 @@ function createDurableAutoExtractor(options) {
17503
17575
  new Promise((_resolve, reject) => {
17504
17576
  watchdog = setTimeout(
17505
17577
  () => reject(new Error("Extraction did not settle in time; retained for retry")),
17506
- options.batchTimeoutMs ?? 18e4
17578
+ options.batchTimeoutMs ?? defaultBatchTimeoutMs(options.extract)
17507
17579
  );
17508
17580
  })
17509
17581
  ]);
17510
17582
  } finally {
17583
+ cancelled = true;
17511
17584
  if (watchdog !== void 0) clearTimeout(watchdog);
17512
17585
  }
17513
17586
  }
17587
+ async function acknowledge(job, ids, loaded, abandoned) {
17588
+ attempts.delete(job.id);
17589
+ sessionFailures.delete(job.id);
17590
+ skippedThisSession.delete(job.id);
17591
+ const sequences = /* @__PURE__ */ new Map();
17592
+ for (const message of loaded)
17593
+ if (message && message.messageId > 0) sequences.set(message.uniqueId, message.messageId);
17594
+ await database.write(async () => {
17595
+ const current = idsOf(job);
17596
+ const acknowledged = ids.filter((id) => current.includes(id));
17597
+ if (!acknowledged.length) return;
17598
+ const done = new Set(acknowledged);
17599
+ const remaining = current.filter((id) => !done.has(id));
17600
+ await job.update((r) => {
17601
+ r._setRaw(
17602
+ "message_ids",
17603
+ JSON.stringify(
17604
+ remaining.length ? [...abandoned ? [] : acknowledged.slice(-2), ...remaining] : []
17605
+ )
17606
+ );
17607
+ const newest = acknowledged[acknowledged.length - 1];
17608
+ r._setRaw("watermark", newest);
17609
+ const sequence = sequences.get(newest);
17610
+ if (sequence !== void 0) {
17611
+ const prior = seqOf(job);
17612
+ r._setRaw("watermark_seq", prior === void 0 ? sequence : Math.max(prior, sequence));
17613
+ }
17614
+ r._setRaw("failed_sessions", abandoned ? MAX_FAILED_SESSIONS : null);
17615
+ r._setRaw("failed_head", abandoned ? newest : null);
17616
+ r._setRaw("failed_at", null);
17617
+ });
17618
+ });
17619
+ }
17620
+ async function recordFailure(job, ids, loaded, error, conversationId) {
17621
+ const head = ids[0];
17622
+ if (!head || !loaded.some(Boolean)) return false;
17623
+ if (error instanceof AccountFailureError) {
17624
+ skippedThisSession.add(job.id);
17625
+ return false;
17626
+ }
17627
+ const counted = error instanceof BatchFailureError || error instanceof LockedSourcesError && error.persistentIds.length > 0;
17628
+ if (!counted) return false;
17629
+ const prior = sessionFailures.get(job.id);
17630
+ const count = prior?.head === head ? prior.count + 1 : 1;
17631
+ sessionFailures.set(job.id, { head, count });
17632
+ const terminal = error instanceof BatchFailureError && error.terminal;
17633
+ if (!terminal && count < MAX_ATTEMPTS) return false;
17634
+ skippedThisSession.add(job.id);
17635
+ const now = clock();
17636
+ const lastCounted = failedAtOf(job, head);
17637
+ if (lastCounted !== void 0 && now - lastCounted < MIN_FAILED_SESSION_GAP_MS) return false;
17638
+ const failures = failedSessionsOf(job, head) + 1;
17639
+ if (failures < MAX_FAILED_SESSIONS) {
17640
+ await database.write(
17641
+ () => job.update((r) => {
17642
+ r._setRaw("failed_sessions", failures);
17643
+ r._setRaw("failed_head", head);
17644
+ r._setRaw("failed_at", now);
17645
+ })
17646
+ );
17647
+ return false;
17648
+ }
17649
+ if (error instanceof LockedSourcesError) {
17650
+ const dropped = new Set(error.persistentIds);
17651
+ let newest;
17652
+ for (const message of loaded)
17653
+ if (message && dropped.has(message.uniqueId) && message.messageId > (newest?.seq ?? 0))
17654
+ newest = { id: message.uniqueId, seq: message.messageId };
17655
+ const advance = newest !== void 0 && newest.seq > (seqOf(job) ?? 0) ? newest : void 0;
17656
+ await database.write(
17657
+ () => job.update((r) => {
17658
+ r._setRaw("message_ids", JSON.stringify(idsOf(job).filter((id) => !dropped.has(id))));
17659
+ if (advance) {
17660
+ r._setRaw("watermark", advance.id);
17661
+ r._setRaw("watermark_seq", advance.seq);
17662
+ }
17663
+ r._setRaw("failed_sessions", advance ? MAX_FAILED_SESSIONS : null);
17664
+ r._setRaw("failed_head", advance ? advance.id : null);
17665
+ r._setRaw("failed_at", null);
17666
+ })
17667
+ );
17668
+ attempts.delete(job.id);
17669
+ sessionFailures.delete(job.id);
17670
+ skippedThisSession.delete(job.id);
17671
+ reportError(
17672
+ new Error(
17673
+ `Dropped ${dropped.size} source id(s) that stayed locked for ${failures} sessions to unblock extraction`
17674
+ ),
17675
+ conversationId
17676
+ );
17677
+ return true;
17678
+ }
17679
+ await acknowledge(job, ids, loaded, true);
17680
+ reportError(
17681
+ new Error(
17682
+ `Abandoned an extraction batch of ${ids.length} source id(s) after it failed in ${failures} sessions`
17683
+ ),
17684
+ conversationId
17685
+ );
17686
+ return true;
17687
+ }
17514
17688
  async function drain() {
17515
17689
  if (disposed || running) return;
17516
17690
  running = true;
@@ -17525,21 +17699,23 @@ function createDurableAutoExtractor(options) {
17525
17699
  for (const job of pending) {
17526
17700
  if (disposed) break;
17527
17701
  const attempted = attempts.get(job.id) ?? 0;
17528
- if (attempted >= MAX_ATTEMPTS) continue;
17702
+ if (attempted >= MAX_ATTEMPTS || skippedThisSession.has(job.id)) continue;
17529
17703
  if (processed >= MAX_JOBS_PER_PASS) {
17530
17704
  scheduleAfterSuccess = true;
17531
17705
  break;
17532
17706
  }
17533
17707
  processed++;
17534
17708
  const conversationId = String(job._getRaw("conversation_id"));
17709
+ let ids = [];
17710
+ let loaded = [];
17535
17711
  try {
17536
17712
  const conversations = await storage.conversationsCollection.query(Q7.where("conversation_id", conversationId), Q7.where("is_deleted", false)).fetchCount();
17537
17713
  if (!conversations) {
17538
17714
  await database.write(() => job.destroyPermanently());
17539
17715
  continue;
17540
17716
  }
17541
- const ids = idsOf(job).slice(0, 20);
17542
- const loaded = await Promise.all(ids.map((id) => getMessageOp(storage, id)));
17717
+ ids = idsOf(job).slice(0, 20);
17718
+ loaded = await Promise.all(ids.map((id) => getMessageOp(storage, id)));
17543
17719
  const missing = ids.filter((_id, index) => loaded[index] === null);
17544
17720
  if (missing.length) {
17545
17721
  if (attempted < MAX_ATTEMPTS - 1)
@@ -17563,17 +17739,21 @@ function createDurableAutoExtractor(options) {
17563
17739
  );
17564
17740
  continue;
17565
17741
  }
17566
- if (loaded.some(
17567
- (message) => message && (message.decryptionStatus || isEncrypted(message.content))
17568
- )) {
17569
- throw new Error("Source messages are locked; retained for retry");
17742
+ const locked = loaded.filter((message) => message && isEncrypted(message.content));
17743
+ if (locked.length) {
17744
+ throw new LockedSourcesError(
17745
+ locked.flatMap(
17746
+ (message) => message.decryptionStatus === "auth_mismatch" || message.decryptionStatus === "invalid_payload" ? [message.uniqueId] : []
17747
+ )
17748
+ );
17570
17749
  }
17571
17750
  const messages = loaded.flatMap(
17572
17751
  (m) => m && m.conversationId === conversationId && (m.role === "user" || m.role === "assistant") ? [{ id: m.uniqueId, role: m.role, content: m.content }] : []
17573
17752
  );
17574
17753
  let success = messages.length === 0;
17754
+ let result;
17575
17755
  if (messages.length) {
17576
- const result = await extractBatch(
17756
+ result = await extractBatch(
17577
17757
  messages,
17578
17758
  conversationId,
17579
17759
  ids,
@@ -17583,41 +17763,35 @@ function createDurableAutoExtractor(options) {
17583
17763
  success = result.failedCount === 0 && result.outcome !== "empty-after-retry";
17584
17764
  options.onTurnComplete?.(result);
17585
17765
  }
17586
- if (!success) throw new Error("Extraction batch incomplete; retained for retry");
17587
- attempts.delete(job.id);
17588
- const sequences = /* @__PURE__ */ new Map();
17589
- for (const message of loaded)
17590
- if (message && message.messageId > 0)
17591
- sequences.set(message.uniqueId, message.messageId);
17592
- await database.write(async () => {
17593
- const current = idsOf(job);
17594
- const acknowledged = ids.filter((id) => current.includes(id));
17595
- if (!acknowledged.length) return;
17596
- const done = new Set(acknowledged);
17597
- const remaining = current.filter((id) => !done.has(id));
17598
- if (remaining.length) {
17599
- await job.update(
17600
- (r) => r._setRaw("message_ids", JSON.stringify([...acknowledged.slice(-2), ...remaining]))
17601
- );
17602
- } else await job.update((r) => r._setRaw("message_ids", "[]"));
17603
- const newest = acknowledged[acknowledged.length - 1];
17604
- await job.update((r) => {
17605
- r._setRaw("watermark", newest);
17606
- const sequence = sequences.get(newest);
17607
- if (sequence !== void 0) {
17608
- const prior = seqOf(job);
17609
- r._setRaw(
17610
- "watermark_seq",
17611
- prior === void 0 ? sequence : Math.max(prior, sequence)
17766
+ if (!success) {
17767
+ const failure = result?.failure;
17768
+ if (failure?.reason === "http-terminal") {
17769
+ const status = failure.httpStatus;
17770
+ if (status !== void 0 && REQUEST_REJECTED_STATUSES.has(status))
17771
+ throw new BatchFailureError(
17772
+ `Extraction rejected the batch (HTTP ${status}); not retried this session`,
17773
+ true
17612
17774
  );
17613
- }
17614
- });
17615
- });
17775
+ throw new AccountFailureError(
17776
+ `Extraction refused for the account (HTTP ${status ?? "unknown"}); retried next session`
17777
+ );
17778
+ }
17779
+ if ((result?.failedCount ?? 0) > 0 || failure !== void 0 && CONTENT_FAILURE_REASONS.has(failure.reason))
17780
+ throw new BatchFailureError("Extraction batch incomplete; retained for retry", false);
17781
+ throw new Error("Extraction batch incomplete; retained for retry");
17782
+ }
17783
+ await acknowledge(job, ids, loaded, false);
17616
17784
  scheduleAfterSuccess = true;
17617
17785
  } catch (error) {
17618
17786
  attempts.set(job.id, attempted + 1);
17619
17787
  retry = true;
17620
17788
  reportError(error, conversationId);
17789
+ try {
17790
+ if (await recordFailure(job, ids, loaded, error, conversationId))
17791
+ scheduleAfterSuccess = true;
17792
+ } catch (recordError) {
17793
+ reportError(recordError, conversationId);
17794
+ }
17621
17795
  }
17622
17796
  }
17623
17797
  } catch (error) {
@@ -17687,7 +17861,7 @@ function createDurableAutoExtractor(options) {
17687
17861
  const start = boundary >= 0 ? boundary + 1 : Math.max(0, ids.length - Math.max(1, options.windowSize ?? 6));
17688
17862
  if (start >= ids.length) return;
17689
17863
  const ownWatermark = job?._getRaw("watermark");
17690
- const atOwnWatermark = !!job && boundary >= 0 && (ids[boundary] === ownWatermark || sequences.get(ids[boundary]) !== void 0 && sequences.get(ids[boundary]) === seqOf(job));
17864
+ const atOwnWatermark = !!job && boundary >= 0 && failedSessionsOf(job, String(ownWatermark)) < MAX_FAILED_SESSIONS && (ids[boundary] === ownWatermark || sequences.get(ids[boundary]) !== void 0 && sequences.get(ids[boundary]) === seqOf(job));
17691
17865
  const overlapStart = atOwnWatermark ? Math.max(0, boundary - 1) : start;
17692
17866
  const fresh = ids.slice(overlapStart).filter((id) => !foreignSources.has(id));
17693
17867
  if (!fresh.length) return;
@@ -18134,6 +18308,9 @@ var MANUAL_FACT_TYPES = [
18134
18308
  function normalizeManualFactType(value) {
18135
18309
  return typeof value === "string" && MANUAL_FACT_TYPES.includes(value) ? value : void 0;
18136
18310
  }
18311
+ function isSharedScope(scope) {
18312
+ return scope === "shared";
18313
+ }
18137
18314
  async function notifyWritten(options, input, outcome) {
18138
18315
  try {
18139
18316
  await options?.onWritten?.({ input, outcome });
@@ -18202,14 +18379,17 @@ function createMemoryVaultTool(vaultCtx, options, embeddingOptions, cache) {
18202
18379
  try {
18203
18380
  const isUpdate = !!id;
18204
18381
  let previousContent;
18382
+ const scope = options?.scope ?? "private";
18205
18383
  if (isUpdate) {
18206
18384
  const existing = await getVaultMemoryOp(vaultCtx, id);
18207
18385
  if (!existing) {
18208
18386
  return `Error: Memory with ID "${id}" not found. Creating a new memory instead would require a separate call without an ID.`;
18209
18387
  }
18388
+ if (isSharedScope(existing.scope) !== isSharedScope(scope)) {
18389
+ return `Error: Memory "${id}" belongs to a different scope than this conversation and cannot be updated from here. It was not modified. To record this fact, save it as a new memory without an ID.`;
18390
+ }
18210
18391
  previousContent = existing.content;
18211
18392
  }
18212
- const scope = options?.scope ?? "private";
18213
18393
  const operation = {
18214
18394
  action: isUpdate ? "update" : "add",
18215
18395
  content,
@@ -21269,7 +21449,7 @@ __decorateClass([
21269
21449
  ], VaultFolder.prototype, "context", 2);
21270
21450
 
21271
21451
  // src/lib/db/schema.ts
21272
- var SDK_SCHEMA_VERSION = 46;
21452
+ var SDK_SCHEMA_VERSION = 47;
21273
21453
  var sdkSchema = appSchema2({
21274
21454
  version: SDK_SCHEMA_VERSION,
21275
21455
  tables: [
@@ -21285,7 +21465,11 @@ var sdkSchema = appSchema2({
21285
21465
  // source. `watermark` alone is an id, and deleting that message erased
21286
21466
  // the anchor — see the column note in extractionJobs/models.ts.
21287
21467
  { name: "watermark_seq", type: "number", isOptional: true },
21288
- { name: "folder_id", type: "string", isOptional: true }
21468
+ { name: "folder_id", type: "string", isOptional: true },
21469
+ // Poison accounting for the head batch; see extractionJobs/models.ts.
21470
+ { name: "failed_sessions", type: "number", isOptional: true },
21471
+ { name: "failed_head", type: "string", isOptional: true },
21472
+ { name: "failed_at", type: "number", isOptional: true }
21289
21473
  ]
21290
21474
  }),
21291
21475
  // Chat storage tables
@@ -22309,6 +22493,22 @@ var sdkMigrations = schemaMigrations2({
22309
22493
  ]
22310
22494
  })
22311
22495
  ]
22496
+ },
22497
+ // v46 -> v47: persisted poison count (and when it was last counted) for a
22498
+ // durable-extraction job's head batch. NULL = never failed, which is right
22499
+ // for every existing row.
22500
+ {
22501
+ toVersion: 47,
22502
+ steps: [
22503
+ addColumns2({
22504
+ table: "memory_extraction_jobs",
22505
+ columns: [
22506
+ { name: "failed_sessions", type: "number", isOptional: true },
22507
+ { name: "failed_head", type: "string", isOptional: true },
22508
+ { name: "failed_at", type: "number", isOptional: true }
22509
+ ]
22510
+ })
22511
+ ]
22312
22512
  }
22313
22513
  ]
22314
22514
  });