@anuma/sdk 6.25.1 → 6.25.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.
@@ -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(
@@ -17416,6 +17440,37 @@ async function assembleMemoryContext(options) {
17416
17440
  import { Q as Q7 } from "@nozbe/watermelondb";
17417
17441
  var MAX_JOBS_PER_PASS = 3;
17418
17442
  var MAX_ATTEMPTS = 3;
17443
+ var MAX_FAILED_SESSIONS = 3;
17444
+ var MIN_FAILED_SESSION_GAP_MS = 60 * 60 * 1e3;
17445
+ var REQUEST_REJECTED_STATUSES = /* @__PURE__ */ new Set([400, 404, 413, 422]);
17446
+ var CONTENT_FAILURE_REASONS = /* @__PURE__ */ new Set([
17447
+ "empty-content",
17448
+ "invalid-json",
17449
+ "null-completion",
17450
+ "body-parse-failed"
17451
+ ]);
17452
+ var BACKOFF_ALLOWANCE_MS = 2100;
17453
+ var RETAIN_BUDGET_MS = 12e4;
17454
+ function defaultBatchTimeoutMs(extract) {
17455
+ const attempts = Math.max(1, extract.maxAttempts ?? 3);
17456
+ const calls = (extract.timeoutMs ?? 6e4) * attempts + BACKOFF_ALLOWANCE_MS * (attempts - 1);
17457
+ const extraction = extract.totalTimeoutMs !== void 0 ? Math.min(extract.totalTimeoutMs, calls) : calls;
17458
+ return extraction + RETAIN_BUDGET_MS;
17459
+ }
17460
+ var BatchFailureError = class extends Error {
17461
+ constructor(message, terminal) {
17462
+ super(message);
17463
+ this.terminal = terminal;
17464
+ }
17465
+ };
17466
+ var AccountFailureError = class extends Error {
17467
+ };
17468
+ var LockedSourcesError = class extends Error {
17469
+ constructor(persistentIds) {
17470
+ super("Source messages are locked; retained for retry");
17471
+ this.persistentIds = persistentIds;
17472
+ }
17473
+ };
17419
17474
  function idsOf(job) {
17420
17475
  return JSON.parse(String(job._getRaw("message_ids")));
17421
17476
  }
@@ -17423,6 +17478,16 @@ function seqOf(job) {
17423
17478
  const raw = job._getRaw("watermark_seq");
17424
17479
  return typeof raw === "number" && raw > 0 ? raw : void 0;
17425
17480
  }
17481
+ function failedSessionsOf(job, head) {
17482
+ if (job._getRaw("failed_head") !== head) return 0;
17483
+ const raw = job._getRaw("failed_sessions");
17484
+ return typeof raw === "number" && raw > 0 ? raw : 0;
17485
+ }
17486
+ function failedAtOf(job, head) {
17487
+ if (job._getRaw("failed_head") !== head) return void 0;
17488
+ const raw = job._getRaw("failed_at");
17489
+ return typeof raw === "number" ? raw : void 0;
17490
+ }
17426
17491
  function createDurableAutoExtractor(options) {
17427
17492
  const vault = options.retainCtx.vaultCtx;
17428
17493
  const database = vault.database;
@@ -17445,6 +17510,9 @@ function createDurableAutoExtractor(options) {
17445
17510
  let writes = Promise.resolve();
17446
17511
  let storeFailures = 0;
17447
17512
  const attempts = /* @__PURE__ */ new Map();
17513
+ const sessionFailures = /* @__PURE__ */ new Map();
17514
+ const skippedThisSession = /* @__PURE__ */ new Set();
17515
+ const clock = options.now ?? Date.now;
17448
17516
  const reportError = (error, conversationId) => {
17449
17517
  try {
17450
17518
  options.onError?.(error instanceof Error ? error : new Error(String(error)), conversationId);
@@ -17473,6 +17541,7 @@ function createDurableAutoExtractor(options) {
17473
17541
  }
17474
17542
  async function extractBatch(messages, conversationId, ids, jobScope, jobFolderId) {
17475
17543
  let watchdog;
17544
+ let cancelled = false;
17476
17545
  const model = options.modelForScope?.(jobScope) ?? options.extract.model;
17477
17546
  try {
17478
17547
  return await Promise.race([
@@ -17483,7 +17552,7 @@ function createDurableAutoExtractor(options) {
17483
17552
  ...options.retainCtx,
17484
17553
  vaultCtx: {
17485
17554
  ...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
17555
+ 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
17556
  }
17488
17557
  },
17489
17558
  // The model follows the job's scope, not the mode alive at drain time.
@@ -17503,14 +17572,116 @@ function createDurableAutoExtractor(options) {
17503
17572
  new Promise((_resolve, reject) => {
17504
17573
  watchdog = setTimeout(
17505
17574
  () => reject(new Error("Extraction did not settle in time; retained for retry")),
17506
- options.batchTimeoutMs ?? 18e4
17575
+ options.batchTimeoutMs ?? defaultBatchTimeoutMs(options.extract)
17507
17576
  );
17508
17577
  })
17509
17578
  ]);
17510
17579
  } finally {
17580
+ cancelled = true;
17511
17581
  if (watchdog !== void 0) clearTimeout(watchdog);
17512
17582
  }
17513
17583
  }
17584
+ async function acknowledge(job, ids, loaded, abandoned) {
17585
+ attempts.delete(job.id);
17586
+ sessionFailures.delete(job.id);
17587
+ skippedThisSession.delete(job.id);
17588
+ const sequences = /* @__PURE__ */ new Map();
17589
+ for (const message of loaded)
17590
+ if (message && message.messageId > 0) sequences.set(message.uniqueId, message.messageId);
17591
+ await database.write(async () => {
17592
+ const current = idsOf(job);
17593
+ const acknowledged = ids.filter((id) => current.includes(id));
17594
+ if (!acknowledged.length) return;
17595
+ const done = new Set(acknowledged);
17596
+ const remaining = current.filter((id) => !done.has(id));
17597
+ await job.update((r) => {
17598
+ r._setRaw(
17599
+ "message_ids",
17600
+ JSON.stringify(
17601
+ remaining.length ? [...abandoned ? [] : acknowledged.slice(-2), ...remaining] : []
17602
+ )
17603
+ );
17604
+ const newest = acknowledged[acknowledged.length - 1];
17605
+ r._setRaw("watermark", newest);
17606
+ const sequence = sequences.get(newest);
17607
+ if (sequence !== void 0) {
17608
+ const prior = seqOf(job);
17609
+ r._setRaw("watermark_seq", prior === void 0 ? sequence : Math.max(prior, sequence));
17610
+ }
17611
+ r._setRaw("failed_sessions", abandoned ? MAX_FAILED_SESSIONS : null);
17612
+ r._setRaw("failed_head", abandoned ? newest : null);
17613
+ r._setRaw("failed_at", null);
17614
+ });
17615
+ });
17616
+ }
17617
+ async function recordFailure(job, ids, loaded, error, conversationId) {
17618
+ const head = ids[0];
17619
+ if (!head || !loaded.some(Boolean)) return false;
17620
+ if (error instanceof AccountFailureError) {
17621
+ skippedThisSession.add(job.id);
17622
+ return false;
17623
+ }
17624
+ const counted = error instanceof BatchFailureError || error instanceof LockedSourcesError && error.persistentIds.length > 0;
17625
+ if (!counted) return false;
17626
+ const prior = sessionFailures.get(job.id);
17627
+ const count = prior?.head === head ? prior.count + 1 : 1;
17628
+ sessionFailures.set(job.id, { head, count });
17629
+ const terminal = error instanceof BatchFailureError && error.terminal;
17630
+ if (!terminal && count < MAX_ATTEMPTS) return false;
17631
+ skippedThisSession.add(job.id);
17632
+ const now = clock();
17633
+ const lastCounted = failedAtOf(job, head);
17634
+ if (lastCounted !== void 0 && now - lastCounted < MIN_FAILED_SESSION_GAP_MS) return false;
17635
+ const failures = failedSessionsOf(job, head) + 1;
17636
+ if (failures < MAX_FAILED_SESSIONS) {
17637
+ await database.write(
17638
+ () => job.update((r) => {
17639
+ r._setRaw("failed_sessions", failures);
17640
+ r._setRaw("failed_head", head);
17641
+ r._setRaw("failed_at", now);
17642
+ })
17643
+ );
17644
+ return false;
17645
+ }
17646
+ if (error instanceof LockedSourcesError) {
17647
+ const dropped = new Set(error.persistentIds);
17648
+ let newest;
17649
+ for (const message of loaded)
17650
+ if (message && dropped.has(message.uniqueId) && message.messageId > (newest?.seq ?? 0))
17651
+ newest = { id: message.uniqueId, seq: message.messageId };
17652
+ const advance = newest !== void 0 && newest.seq > (seqOf(job) ?? 0) ? newest : void 0;
17653
+ await database.write(
17654
+ () => job.update((r) => {
17655
+ r._setRaw("message_ids", JSON.stringify(idsOf(job).filter((id) => !dropped.has(id))));
17656
+ if (advance) {
17657
+ r._setRaw("watermark", advance.id);
17658
+ r._setRaw("watermark_seq", advance.seq);
17659
+ }
17660
+ r._setRaw("failed_sessions", advance ? MAX_FAILED_SESSIONS : null);
17661
+ r._setRaw("failed_head", advance ? advance.id : null);
17662
+ r._setRaw("failed_at", null);
17663
+ })
17664
+ );
17665
+ attempts.delete(job.id);
17666
+ sessionFailures.delete(job.id);
17667
+ skippedThisSession.delete(job.id);
17668
+ reportError(
17669
+ new Error(
17670
+ `Dropped ${dropped.size} source id(s) that stayed locked for ${failures} sessions to unblock extraction`
17671
+ ),
17672
+ conversationId
17673
+ );
17674
+ return true;
17675
+ }
17676
+ await acknowledge(job, ids, loaded, true);
17677
+ reportError(
17678
+ new Error(
17679
+ `Abandoned an extraction batch of ${ids.length} source id(s) after it failed in ${failures} sessions`
17680
+ ),
17681
+ conversationId
17682
+ );
17683
+ return true;
17684
+ }
17514
17685
  async function drain() {
17515
17686
  if (disposed || running) return;
17516
17687
  running = true;
@@ -17525,21 +17696,23 @@ function createDurableAutoExtractor(options) {
17525
17696
  for (const job of pending) {
17526
17697
  if (disposed) break;
17527
17698
  const attempted = attempts.get(job.id) ?? 0;
17528
- if (attempted >= MAX_ATTEMPTS) continue;
17699
+ if (attempted >= MAX_ATTEMPTS || skippedThisSession.has(job.id)) continue;
17529
17700
  if (processed >= MAX_JOBS_PER_PASS) {
17530
17701
  scheduleAfterSuccess = true;
17531
17702
  break;
17532
17703
  }
17533
17704
  processed++;
17534
17705
  const conversationId = String(job._getRaw("conversation_id"));
17706
+ let ids = [];
17707
+ let loaded = [];
17535
17708
  try {
17536
17709
  const conversations = await storage.conversationsCollection.query(Q7.where("conversation_id", conversationId), Q7.where("is_deleted", false)).fetchCount();
17537
17710
  if (!conversations) {
17538
17711
  await database.write(() => job.destroyPermanently());
17539
17712
  continue;
17540
17713
  }
17541
- const ids = idsOf(job).slice(0, 20);
17542
- const loaded = await Promise.all(ids.map((id) => getMessageOp(storage, id)));
17714
+ ids = idsOf(job).slice(0, 20);
17715
+ loaded = await Promise.all(ids.map((id) => getMessageOp(storage, id)));
17543
17716
  const missing = ids.filter((_id, index) => loaded[index] === null);
17544
17717
  if (missing.length) {
17545
17718
  if (attempted < MAX_ATTEMPTS - 1)
@@ -17563,17 +17736,21 @@ function createDurableAutoExtractor(options) {
17563
17736
  );
17564
17737
  continue;
17565
17738
  }
17566
- if (loaded.some(
17567
- (message) => message && (message.decryptionStatus || isEncrypted(message.content))
17568
- )) {
17569
- throw new Error("Source messages are locked; retained for retry");
17739
+ const locked = loaded.filter((message) => message && isEncrypted(message.content));
17740
+ if (locked.length) {
17741
+ throw new LockedSourcesError(
17742
+ locked.flatMap(
17743
+ (message) => message.decryptionStatus === "auth_mismatch" || message.decryptionStatus === "invalid_payload" ? [message.uniqueId] : []
17744
+ )
17745
+ );
17570
17746
  }
17571
17747
  const messages = loaded.flatMap(
17572
17748
  (m) => m && m.conversationId === conversationId && (m.role === "user" || m.role === "assistant") ? [{ id: m.uniqueId, role: m.role, content: m.content }] : []
17573
17749
  );
17574
17750
  let success = messages.length === 0;
17751
+ let result;
17575
17752
  if (messages.length) {
17576
- const result = await extractBatch(
17753
+ result = await extractBatch(
17577
17754
  messages,
17578
17755
  conversationId,
17579
17756
  ids,
@@ -17583,41 +17760,35 @@ function createDurableAutoExtractor(options) {
17583
17760
  success = result.failedCount === 0 && result.outcome !== "empty-after-retry";
17584
17761
  options.onTurnComplete?.(result);
17585
17762
  }
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)
17763
+ if (!success) {
17764
+ const failure = result?.failure;
17765
+ if (failure?.reason === "http-terminal") {
17766
+ const status = failure.httpStatus;
17767
+ if (status !== void 0 && REQUEST_REJECTED_STATUSES.has(status))
17768
+ throw new BatchFailureError(
17769
+ `Extraction rejected the batch (HTTP ${status}); not retried this session`,
17770
+ true
17612
17771
  );
17613
- }
17614
- });
17615
- });
17772
+ throw new AccountFailureError(
17773
+ `Extraction refused for the account (HTTP ${status ?? "unknown"}); retried next session`
17774
+ );
17775
+ }
17776
+ if ((result?.failedCount ?? 0) > 0 || failure !== void 0 && CONTENT_FAILURE_REASONS.has(failure.reason))
17777
+ throw new BatchFailureError("Extraction batch incomplete; retained for retry", false);
17778
+ throw new Error("Extraction batch incomplete; retained for retry");
17779
+ }
17780
+ await acknowledge(job, ids, loaded, false);
17616
17781
  scheduleAfterSuccess = true;
17617
17782
  } catch (error) {
17618
17783
  attempts.set(job.id, attempted + 1);
17619
17784
  retry = true;
17620
17785
  reportError(error, conversationId);
17786
+ try {
17787
+ if (await recordFailure(job, ids, loaded, error, conversationId))
17788
+ scheduleAfterSuccess = true;
17789
+ } catch (recordError) {
17790
+ reportError(recordError, conversationId);
17791
+ }
17621
17792
  }
17622
17793
  }
17623
17794
  } catch (error) {
@@ -17687,7 +17858,7 @@ function createDurableAutoExtractor(options) {
17687
17858
  const start = boundary >= 0 ? boundary + 1 : Math.max(0, ids.length - Math.max(1, options.windowSize ?? 6));
17688
17859
  if (start >= ids.length) return;
17689
17860
  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));
17861
+ 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
17862
  const overlapStart = atOwnWatermark ? Math.max(0, boundary - 1) : start;
17692
17863
  const fresh = ids.slice(overlapStart).filter((id) => !foreignSources.has(id));
17693
17864
  if (!fresh.length) return;
@@ -18134,6 +18305,9 @@ var MANUAL_FACT_TYPES = [
18134
18305
  function normalizeManualFactType(value) {
18135
18306
  return typeof value === "string" && MANUAL_FACT_TYPES.includes(value) ? value : void 0;
18136
18307
  }
18308
+ function isSharedScope(scope) {
18309
+ return scope === "shared";
18310
+ }
18137
18311
  async function notifyWritten(options, input, outcome) {
18138
18312
  try {
18139
18313
  await options?.onWritten?.({ input, outcome });
@@ -18202,14 +18376,17 @@ function createMemoryVaultTool(vaultCtx, options, embeddingOptions, cache) {
18202
18376
  try {
18203
18377
  const isUpdate = !!id;
18204
18378
  let previousContent;
18379
+ const scope = options?.scope ?? "private";
18205
18380
  if (isUpdate) {
18206
18381
  const existing = await getVaultMemoryOp(vaultCtx, id);
18207
18382
  if (!existing) {
18208
18383
  return `Error: Memory with ID "${id}" not found. Creating a new memory instead would require a separate call without an ID.`;
18209
18384
  }
18385
+ if (isSharedScope(existing.scope) !== isSharedScope(scope)) {
18386
+ 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.`;
18387
+ }
18210
18388
  previousContent = existing.content;
18211
18389
  }
18212
- const scope = options?.scope ?? "private";
18213
18390
  const operation = {
18214
18391
  action: isUpdate ? "update" : "add",
18215
18392
  content,
@@ -21269,7 +21446,7 @@ __decorateClass([
21269
21446
  ], VaultFolder.prototype, "context", 2);
21270
21447
 
21271
21448
  // src/lib/db/schema.ts
21272
- var SDK_SCHEMA_VERSION = 46;
21449
+ var SDK_SCHEMA_VERSION = 47;
21273
21450
  var sdkSchema = appSchema2({
21274
21451
  version: SDK_SCHEMA_VERSION,
21275
21452
  tables: [
@@ -21285,7 +21462,11 @@ var sdkSchema = appSchema2({
21285
21462
  // source. `watermark` alone is an id, and deleting that message erased
21286
21463
  // the anchor — see the column note in extractionJobs/models.ts.
21287
21464
  { name: "watermark_seq", type: "number", isOptional: true },
21288
- { name: "folder_id", type: "string", isOptional: true }
21465
+ { name: "folder_id", type: "string", isOptional: true },
21466
+ // Poison accounting for the head batch; see extractionJobs/models.ts.
21467
+ { name: "failed_sessions", type: "number", isOptional: true },
21468
+ { name: "failed_head", type: "string", isOptional: true },
21469
+ { name: "failed_at", type: "number", isOptional: true }
21289
21470
  ]
21290
21471
  }),
21291
21472
  // Chat storage tables
@@ -22309,6 +22490,22 @@ var sdkMigrations = schemaMigrations2({
22309
22490
  ]
22310
22491
  })
22311
22492
  ]
22493
+ },
22494
+ // v46 -> v47: persisted poison count (and when it was last counted) for a
22495
+ // durable-extraction job's head batch. NULL = never failed, which is right
22496
+ // for every existing row.
22497
+ {
22498
+ toVersion: 47,
22499
+ steps: [
22500
+ addColumns2({
22501
+ table: "memory_extraction_jobs",
22502
+ columns: [
22503
+ { name: "failed_sessions", type: "number", isOptional: true },
22504
+ { name: "failed_head", type: "string", isOptional: true },
22505
+ { name: "failed_at", type: "number", isOptional: true }
22506
+ ]
22507
+ })
22508
+ ]
22312
22509
  }
22313
22510
  ]
22314
22511
  });