@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.
@@ -13014,8 +13014,42 @@ async function vaultMemoryToStored(memory, walletAddress, signMessage, embeddedW
13014
13014
  }
13015
13015
  return raw;
13016
13016
  }
13017
+ function populateNewVaultMemory(record, ctx, opts, encryptedContent) {
13018
+ record._setRaw("content", encryptedContent);
13019
+ record._setRaw("scope", opts.scope ?? "private");
13020
+ record._setRaw("folder_id", opts.folderId ?? null);
13021
+ record._setRaw("user_id", ctx.userId ?? null);
13022
+ record._setRaw("is_deleted", false);
13023
+ if (opts.embedding !== void 0) {
13024
+ record._setRaw("embedding", opts.embedding);
13025
+ record._setRaw("embedding_model", opts.embeddingModel ?? null);
13026
+ }
13027
+ if (opts.sourceChunkIds !== void 0) {
13028
+ record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
13029
+ }
13030
+ record._setRaw("proof_count", opts.proofCount ?? 1);
13031
+ record._setRaw("source", opts.source ?? "manual");
13032
+ if (opts.eventTime) {
13033
+ record._setRaw("event_time_start", opts.eventTime.start ?? null);
13034
+ record._setRaw("event_time_end", opts.eventTime.end ?? null);
13035
+ record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
13036
+ }
13037
+ if (opts.factType !== void 0) {
13038
+ record._setRaw("fact_type", opts.factType);
13039
+ }
13040
+ if (opts.trustTier !== void 0) {
13041
+ record._setRaw("trust_tier", normalizeTrustTier(opts.trustTier));
13042
+ }
13043
+ record._setRaw("visibility", opts.visibility ?? "private");
13044
+ record._setRaw(
13045
+ "published_at",
13046
+ opts.visibility && opts.visibility !== "private" ? opts.publishedAt ?? Date.now() : null
13047
+ );
13048
+ if (opts.geohash !== void 0) {
13049
+ record._setRaw("geohash", opts.geohash);
13050
+ }
13051
+ }
13017
13052
  async function createVaultMemoryOp(ctx, opts) {
13018
- const scope = opts.scope ?? "private";
13019
13053
  const encryptedContent = ctx.walletAddress && ctx.signMessage ? await encryptVaultMemoryContent(
13020
13054
  opts.content,
13021
13055
  ctx.walletAddress,
@@ -13025,47 +13059,14 @@ async function createVaultMemoryOp(ctx, opts) {
13025
13059
  const created = await ctx.database.write(async () => {
13026
13060
  if (ctx.canWrite && !await ctx.canWrite())
13027
13061
  throw new Error("Memory source is no longer eligible");
13028
- return ctx.vaultMemoryCollection.create((record) => {
13029
- record._setRaw("content", encryptedContent);
13030
- record._setRaw("scope", scope);
13031
- record._setRaw("folder_id", opts.folderId ?? null);
13032
- record._setRaw("user_id", ctx.userId ?? null);
13033
- record._setRaw("is_deleted", false);
13034
- if (opts.embedding !== void 0) {
13035
- record._setRaw("embedding", opts.embedding);
13036
- record._setRaw("embedding_model", opts.embeddingModel ?? null);
13037
- }
13038
- if (opts.sourceChunkIds !== void 0) {
13039
- record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
13040
- }
13041
- record._setRaw("proof_count", opts.proofCount ?? 1);
13042
- record._setRaw("source", opts.source ?? "manual");
13043
- if (opts.eventTime) {
13044
- record._setRaw("event_time_start", opts.eventTime.start ?? null);
13045
- record._setRaw("event_time_end", opts.eventTime.end ?? null);
13046
- record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
13047
- }
13048
- if (opts.factType !== void 0) {
13049
- record._setRaw("fact_type", opts.factType);
13050
- }
13051
- if (opts.trustTier !== void 0) {
13052
- record._setRaw("trust_tier", normalizeTrustTier(opts.trustTier));
13053
- }
13054
- record._setRaw("visibility", opts.visibility ?? "private");
13055
- record._setRaw(
13056
- "published_at",
13057
- opts.visibility && opts.visibility !== "private" ? opts.publishedAt ?? Date.now() : null
13058
- );
13059
- if (opts.geohash !== void 0) {
13060
- record._setRaw("geohash", opts.geohash);
13061
- }
13062
- });
13062
+ return ctx.vaultMemoryCollection.create(
13063
+ (record) => populateNewVaultMemory(record, ctx, opts, encryptedContent)
13064
+ );
13063
13065
  });
13064
13066
  return vaultMemoryToStored(created, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner);
13065
13067
  }
13066
13068
  async function createSupersedingMemoryOp(ctx, opts, targetId) {
13067
13069
  if (!targetId) return { created: null, retired: false };
13068
- const scope = opts.scope ?? "private";
13069
13070
  const encryptedContent = ctx.walletAddress && ctx.signMessage ? await encryptVaultMemoryContent(
13070
13071
  opts.content,
13071
13072
  ctx.walletAddress,
@@ -13083,27 +13084,9 @@ async function createSupersedingMemoryOp(ctx, opts, targetId) {
13083
13084
  return;
13084
13085
  }
13085
13086
  if (target.isDeleted || target.supersededBy || !isOwnedByCtxUser(ctx, target)) return;
13086
- createdRecord = await ctx.vaultMemoryCollection.create((record) => {
13087
- record._setRaw("content", encryptedContent);
13088
- record._setRaw("scope", scope);
13089
- record._setRaw("folder_id", opts.folderId ?? null);
13090
- record._setRaw("user_id", ctx.userId ?? null);
13091
- record._setRaw("is_deleted", false);
13092
- if (opts.embedding !== void 0) {
13093
- record._setRaw("embedding", opts.embedding);
13094
- record._setRaw("embedding_model", opts.embeddingModel ?? null);
13095
- }
13096
- if (opts.sourceChunkIds !== void 0) {
13097
- record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
13098
- }
13099
- record._setRaw("proof_count", opts.proofCount ?? 1);
13100
- record._setRaw("source", opts.source ?? "manual");
13101
- if (opts.eventTime) {
13102
- record._setRaw("event_time_start", opts.eventTime.start ?? null);
13103
- record._setRaw("event_time_end", opts.eventTime.end ?? null);
13104
- record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
13105
- }
13106
- });
13087
+ createdRecord = await ctx.vaultMemoryCollection.create(
13088
+ (record) => populateNewVaultMemory(record, ctx, opts, encryptedContent)
13089
+ );
13107
13090
  await target.update((r) => {
13108
13091
  r._setRaw("superseded_by", createdRecord.id);
13109
13092
  r._setRaw("superseded_at", Date.now());
@@ -13183,41 +13166,9 @@ async function createVaultMemoriesBatchOp(ctx, optionsArray) {
13183
13166
  if (ctx.canWrite && !await ctx.canWrite())
13184
13167
  throw new Error("Memory source is no longer eligible");
13185
13168
  const prepared = optionsArray.map(
13186
- (opts, i) => ctx.vaultMemoryCollection.prepareCreate((record) => {
13187
- record._setRaw("content", encryptedContents[i]);
13188
- record._setRaw("scope", opts.scope ?? "private");
13189
- record._setRaw("folder_id", opts.folderId ?? null);
13190
- record._setRaw("user_id", ctx.userId ?? null);
13191
- record._setRaw("is_deleted", false);
13192
- if (optionsArray[i].embedding !== void 0) {
13193
- record._setRaw("embedding", optionsArray[i].embedding);
13194
- record._setRaw("embedding_model", optionsArray[i].embeddingModel ?? null);
13195
- }
13196
- if (opts.sourceChunkIds !== void 0) {
13197
- record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
13198
- }
13199
- record._setRaw("proof_count", opts.proofCount ?? 1);
13200
- record._setRaw("source", opts.source ?? "manual");
13201
- if (opts.eventTime) {
13202
- record._setRaw("event_time_start", opts.eventTime.start ?? null);
13203
- record._setRaw("event_time_end", opts.eventTime.end ?? null);
13204
- record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
13205
- }
13206
- if (opts.factType !== void 0) {
13207
- record._setRaw("fact_type", opts.factType);
13208
- }
13209
- if (opts.trustTier !== void 0) {
13210
- record._setRaw("trust_tier", normalizeTrustTier(opts.trustTier));
13211
- }
13212
- record._setRaw("visibility", opts.visibility ?? "private");
13213
- record._setRaw(
13214
- "published_at",
13215
- opts.visibility && opts.visibility !== "private" ? opts.publishedAt ?? Date.now() : null
13216
- );
13217
- if (opts.geohash !== void 0) {
13218
- record._setRaw("geohash", opts.geohash);
13219
- }
13220
- })
13169
+ (opts, i) => ctx.vaultMemoryCollection.prepareCreate(
13170
+ (record) => populateNewVaultMemory(record, ctx, opts, encryptedContents[i])
13171
+ )
13221
13172
  );
13222
13173
  await ctx.database.batch(...prepared);
13223
13174
  return prepared;
@@ -14083,21 +14034,73 @@ async function hardDeleteDecayedOp(ctx, id, opts) {
14083
14034
  return false;
14084
14035
  }
14085
14036
  }
14086
- async function updateVaultMemoryEmbeddingOp(ctx, id, embedding, embeddingModel) {
14037
+ async function updateVaultMemoryEmbeddingOp(ctx, id, embedding, embeddingModel, expected) {
14087
14038
  try {
14088
14039
  const record = await ctx.vaultMemoryCollection.find(id);
14089
14040
  if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) return false;
14041
+ const readUpdatedAt = record.updatedAt.getTime();
14042
+ const readStoredContent = record._getRaw("content");
14043
+ if (expected?.updatedAt !== void 0 && expected.updatedAt !== readUpdatedAt) return false;
14044
+ if (expected?.content !== void 0) {
14045
+ const current = await vaultMemoryToStored(
14046
+ record,
14047
+ ctx.walletAddress,
14048
+ ctx.signMessage,
14049
+ ctx.embeddedWalletSigner
14050
+ );
14051
+ if (current.content !== expected.content) return false;
14052
+ }
14053
+ let written = false;
14090
14054
  await ctx.database.write(async () => {
14055
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) return;
14056
+ const originalUpdatedAt = record.updatedAt.getTime();
14057
+ if (originalUpdatedAt !== readUpdatedAt || record._getRaw("content") !== readStoredContent)
14058
+ return;
14091
14059
  await record.update((r) => {
14092
14060
  r._setRaw("embedding", embedding);
14093
14061
  r._setRaw("embedding_model", embeddingModel);
14062
+ r._setRaw("updated_at", originalUpdatedAt);
14094
14063
  });
14064
+ written = true;
14095
14065
  });
14096
- return true;
14066
+ return written;
14097
14067
  } catch {
14098
14068
  return false;
14099
14069
  }
14100
14070
  }
14071
+ function normalizeForDedupe(content) {
14072
+ return content.normalize("NFKC").trim().replace(/\s+/g, " ").toLowerCase();
14073
+ }
14074
+ async function findQuarantinedDuplicateOp(ctx, content, sourceIds, where) {
14075
+ const wanted = [...new Set(sourceIds)].sort().join("\0");
14076
+ const target = normalizeForDedupe(content);
14077
+ const rows = await ctx.vaultMemoryCollection.query(
14078
+ import_watermelondb11.Q.where("trust_tier", "quarantined"),
14079
+ import_watermelondb11.Q.where("is_deleted", false),
14080
+ import_watermelondb11.Q.where("scope", where.scope),
14081
+ import_watermelondb11.Q.where("folder_id", where.folderId),
14082
+ ...ctx.userId !== void 0 ? [import_watermelondb11.Q.where("user_id", ctx.userId)] : []
14083
+ ).unsafeFetchRaw();
14084
+ for (const raw of rows) {
14085
+ let ids;
14086
+ try {
14087
+ ids = JSON.parse(raw.source_chunk_ids ?? "[]");
14088
+ } catch {
14089
+ continue;
14090
+ }
14091
+ if (!Array.isArray(ids)) continue;
14092
+ const key = [...new Set(ids.filter((id) => typeof id === "string"))].sort().join("\0");
14093
+ if (key !== wanted) continue;
14094
+ const stored = await vaultMemoryRawToStored(
14095
+ raw,
14096
+ ctx.walletAddress,
14097
+ ctx.signMessage,
14098
+ ctx.embeddedWalletSigner
14099
+ );
14100
+ if (normalizeForDedupe(stored.content) === target) return stored.uniqueId;
14101
+ }
14102
+ return null;
14103
+ }
14101
14104
 
14102
14105
  // src/lib/memory/decay.ts
14103
14106
  init_logger();
@@ -14122,6 +14125,10 @@ var DEFAULT_DECAY_POLICY = {
14122
14125
  hardDeleteWindowMs: HARD_DELETE_WINDOW_MS,
14123
14126
  pastEventGraceMs: PAST_EVENT_GRACE_MS
14124
14127
  };
14128
+ function lastActivityAt(m) {
14129
+ const observedAt = Number.isFinite(m.lastObservedAt) ? m.lastObservedAt : m.updatedAt;
14130
+ return Math.max(m.updatedAt, observedAt);
14131
+ }
14125
14132
  function resolvePolicy(policy) {
14126
14133
  if (!policy) return DEFAULT_DECAY_POLICY;
14127
14134
  return {
@@ -14161,8 +14168,7 @@ function classifyDecay(m, now, policy) {
14161
14168
  if (m.eventTimeEnd < now - resolved.pastEventGraceMs) return "archive";
14162
14169
  if (m.eventTimeEnd >= now) return "keep";
14163
14170
  }
14164
- const observedAt = Number.isFinite(m.lastObservedAt) ? m.lastObservedAt : m.updatedAt;
14165
- if (now - Math.max(m.updatedAt, observedAt) > ttlForType(m.factType, resolved)) return "archive";
14171
+ if (now - lastActivityAt(m) > ttlForType(m.factType, resolved)) return "archive";
14166
14172
  return "keep";
14167
14173
  }
14168
14174
 
@@ -17188,6 +17194,24 @@ async function extractAndRetain(messages, retainCtx, options) {
17188
17194
  const tRetain = Date.now();
17189
17195
  for (const { candidate, isQuarantined, reason, signature } of toRetain) {
17190
17196
  try {
17197
+ if (isQuarantined) {
17198
+ const existingId = await findQuarantinedDuplicateOp(
17199
+ retainCtx.vaultCtx,
17200
+ candidate.content,
17201
+ candidate.sourceMessageIds,
17202
+ // The same defaults retain() writes the row with.
17203
+ { scope: options.scope ?? "private", folderId: options.folderId ?? null }
17204
+ ).catch(() => null);
17205
+ if (existingId) {
17206
+ quarantinedInfo.push({
17207
+ candidate,
17208
+ memoryId: existingId,
17209
+ reason,
17210
+ signature
17211
+ });
17212
+ continue;
17213
+ }
17214
+ }
17191
17215
  const result = await retain(candidate.content, retainCtx, {
17192
17216
  source: "auto-extracted",
17193
17217
  sourceChunkIds: candidate.sourceMessageIds,
@@ -17638,7 +17662,7 @@ function createLlmDecayClassifier(options) {
17638
17662
  if (!content || content.trim().length === 0) return ruleVerdict;
17639
17663
  const trimmed = content.trim().slice(0, MAX_CONTENT_CHARS);
17640
17664
  const safe = redactor ? (await redactor.redactTextAsync(trimmed)).text : trimmed;
17641
- const meta = `factType: ${input.factType ?? "none"}; ageDays: ${ageDays(input.updatedAt, now)}`;
17665
+ const meta = `factType: ${input.factType ?? "none"}; ageDays: ${ageDays(lastActivityAt(input), now)}`;
17642
17666
  let parsed;
17643
17667
  try {
17644
17668
  parsed = await callPortalJsonCompletion({
@@ -17671,9 +17695,9 @@ Should this be kept or archived?`,
17671
17695
  }
17672
17696
  };
17673
17697
  }
17674
- function ageDays(updatedAt, now) {
17675
- if (!Number.isFinite(updatedAt) || !Number.isFinite(now)) return 0;
17676
- return Math.max(0, Math.floor((now - updatedAt) / (24 * 60 * 60 * 1e3)));
17698
+ function ageDays(activityAt, now) {
17699
+ if (!Number.isFinite(activityAt) || !Number.isFinite(now)) return 0;
17700
+ return Math.max(0, Math.floor((now - activityAt) / (24 * 60 * 60 * 1e3)));
17677
17701
  }
17678
17702
  function parseVerdict(parsed) {
17679
17703
  if (typeof parsed !== "object" || parsed === null) return null;
@@ -17727,7 +17751,7 @@ function createDecaySweeper(options) {
17727
17751
  if (!classifier || !isBorderline(input)) return ruleVerdict;
17728
17752
  if (input.id) {
17729
17753
  const cached = classifierCache.get(input.id);
17730
- if (cached && cached.updatedAt === input.updatedAt) return cached.verdict;
17754
+ if (cached && cached.activityAt === lastActivityAt(input)) return cached.verdict;
17731
17755
  }
17732
17756
  if (sweep.classifierCalls >= maxClassifierCalls) {
17733
17757
  if (!sweep.ceilingLogged) {
@@ -17741,7 +17765,7 @@ function createDecaySweeper(options) {
17741
17765
  sweep.classifierCalls++;
17742
17766
  try {
17743
17767
  const verdict = await classifier.classify(input, ruleVerdict, now);
17744
- if (input.id) classifierCache.set(input.id, { updatedAt: input.updatedAt, verdict });
17768
+ if (input.id) classifierCache.set(input.id, { activityAt: lastActivityAt(input), verdict });
17745
17769
  return verdict;
17746
17770
  } catch (err) {
17747
17771
  getLogger().warn(
@@ -20378,6 +20402,37 @@ async function assembleMemoryContext(options) {
20378
20402
  var import_watermelondb13 = require("@nozbe/watermelondb");
20379
20403
  var MAX_JOBS_PER_PASS = 3;
20380
20404
  var MAX_ATTEMPTS = 3;
20405
+ var MAX_FAILED_SESSIONS = 3;
20406
+ var MIN_FAILED_SESSION_GAP_MS = 60 * 60 * 1e3;
20407
+ var REQUEST_REJECTED_STATUSES = /* @__PURE__ */ new Set([400, 404, 413, 422]);
20408
+ var CONTENT_FAILURE_REASONS = /* @__PURE__ */ new Set([
20409
+ "empty-content",
20410
+ "invalid-json",
20411
+ "null-completion",
20412
+ "body-parse-failed"
20413
+ ]);
20414
+ var BACKOFF_ALLOWANCE_MS = 2100;
20415
+ var RETAIN_BUDGET_MS = 12e4;
20416
+ function defaultBatchTimeoutMs(extract) {
20417
+ const attempts = Math.max(1, extract.maxAttempts ?? 3);
20418
+ const calls = (extract.timeoutMs ?? 6e4) * attempts + BACKOFF_ALLOWANCE_MS * (attempts - 1);
20419
+ const extraction = extract.totalTimeoutMs !== void 0 ? Math.min(extract.totalTimeoutMs, calls) : calls;
20420
+ return extraction + RETAIN_BUDGET_MS;
20421
+ }
20422
+ var BatchFailureError = class extends Error {
20423
+ constructor(message, terminal) {
20424
+ super(message);
20425
+ this.terminal = terminal;
20426
+ }
20427
+ };
20428
+ var AccountFailureError = class extends Error {
20429
+ };
20430
+ var LockedSourcesError = class extends Error {
20431
+ constructor(persistentIds) {
20432
+ super("Source messages are locked; retained for retry");
20433
+ this.persistentIds = persistentIds;
20434
+ }
20435
+ };
20381
20436
  function idsOf(job) {
20382
20437
  return JSON.parse(String(job._getRaw("message_ids")));
20383
20438
  }
@@ -20385,6 +20440,16 @@ function seqOf(job) {
20385
20440
  const raw = job._getRaw("watermark_seq");
20386
20441
  return typeof raw === "number" && raw > 0 ? raw : void 0;
20387
20442
  }
20443
+ function failedSessionsOf(job, head) {
20444
+ if (job._getRaw("failed_head") !== head) return 0;
20445
+ const raw = job._getRaw("failed_sessions");
20446
+ return typeof raw === "number" && raw > 0 ? raw : 0;
20447
+ }
20448
+ function failedAtOf(job, head) {
20449
+ if (job._getRaw("failed_head") !== head) return void 0;
20450
+ const raw = job._getRaw("failed_at");
20451
+ return typeof raw === "number" ? raw : void 0;
20452
+ }
20388
20453
  function createDurableAutoExtractor(options) {
20389
20454
  const vault = options.retainCtx.vaultCtx;
20390
20455
  const database = vault.database;
@@ -20407,6 +20472,9 @@ function createDurableAutoExtractor(options) {
20407
20472
  let writes = Promise.resolve();
20408
20473
  let storeFailures = 0;
20409
20474
  const attempts = /* @__PURE__ */ new Map();
20475
+ const sessionFailures = /* @__PURE__ */ new Map();
20476
+ const skippedThisSession = /* @__PURE__ */ new Set();
20477
+ const clock = options.now ?? Date.now;
20410
20478
  const reportError = (error, conversationId) => {
20411
20479
  try {
20412
20480
  options.onError?.(error instanceof Error ? error : new Error(String(error)), conversationId);
@@ -20435,6 +20503,7 @@ function createDurableAutoExtractor(options) {
20435
20503
  }
20436
20504
  async function extractBatch(messages, conversationId, ids, jobScope, jobFolderId) {
20437
20505
  let watchdog;
20506
+ let cancelled = false;
20438
20507
  const model = options.modelForScope?.(jobScope) ?? options.extract.model;
20439
20508
  try {
20440
20509
  return await Promise.race([
@@ -20445,7 +20514,7 @@ function createDurableAutoExtractor(options) {
20445
20514
  ...options.retainCtx,
20446
20515
  vaultCtx: {
20447
20516
  ...vault,
20448
- canWrite: async () => !disposed && await storage.conversationsCollection.query(import_watermelondb13.Q.where("conversation_id", conversationId), import_watermelondb13.Q.where("is_deleted", false)).fetchCount() > 0 && await storage.messagesCollection.query(import_watermelondb13.Q.where("conversation_id", conversationId), import_watermelondb13.Q.where("id", import_watermelondb13.Q.oneOf(ids))).fetchCount() === ids.length && (!vault.canWrite || await vault.canWrite()) && !disposed
20517
+ canWrite: async () => !disposed && !cancelled && await storage.conversationsCollection.query(import_watermelondb13.Q.where("conversation_id", conversationId), import_watermelondb13.Q.where("is_deleted", false)).fetchCount() > 0 && await storage.messagesCollection.query(import_watermelondb13.Q.where("conversation_id", conversationId), import_watermelondb13.Q.where("id", import_watermelondb13.Q.oneOf(ids))).fetchCount() === ids.length && (!vault.canWrite || await vault.canWrite()) && !disposed && !cancelled
20449
20518
  }
20450
20519
  },
20451
20520
  // The model follows the job's scope, not the mode alive at drain time.
@@ -20465,14 +20534,116 @@ function createDurableAutoExtractor(options) {
20465
20534
  new Promise((_resolve, reject) => {
20466
20535
  watchdog = setTimeout(
20467
20536
  () => reject(new Error("Extraction did not settle in time; retained for retry")),
20468
- options.batchTimeoutMs ?? 18e4
20537
+ options.batchTimeoutMs ?? defaultBatchTimeoutMs(options.extract)
20469
20538
  );
20470
20539
  })
20471
20540
  ]);
20472
20541
  } finally {
20542
+ cancelled = true;
20473
20543
  if (watchdog !== void 0) clearTimeout(watchdog);
20474
20544
  }
20475
20545
  }
20546
+ async function acknowledge(job, ids, loaded, abandoned) {
20547
+ attempts.delete(job.id);
20548
+ sessionFailures.delete(job.id);
20549
+ skippedThisSession.delete(job.id);
20550
+ const sequences = /* @__PURE__ */ new Map();
20551
+ for (const message of loaded)
20552
+ if (message && message.messageId > 0) sequences.set(message.uniqueId, message.messageId);
20553
+ await database.write(async () => {
20554
+ const current = idsOf(job);
20555
+ const acknowledged = ids.filter((id) => current.includes(id));
20556
+ if (!acknowledged.length) return;
20557
+ const done = new Set(acknowledged);
20558
+ const remaining = current.filter((id) => !done.has(id));
20559
+ await job.update((r) => {
20560
+ r._setRaw(
20561
+ "message_ids",
20562
+ JSON.stringify(
20563
+ remaining.length ? [...abandoned ? [] : acknowledged.slice(-2), ...remaining] : []
20564
+ )
20565
+ );
20566
+ const newest = acknowledged[acknowledged.length - 1];
20567
+ r._setRaw("watermark", newest);
20568
+ const sequence = sequences.get(newest);
20569
+ if (sequence !== void 0) {
20570
+ const prior = seqOf(job);
20571
+ r._setRaw("watermark_seq", prior === void 0 ? sequence : Math.max(prior, sequence));
20572
+ }
20573
+ r._setRaw("failed_sessions", abandoned ? MAX_FAILED_SESSIONS : null);
20574
+ r._setRaw("failed_head", abandoned ? newest : null);
20575
+ r._setRaw("failed_at", null);
20576
+ });
20577
+ });
20578
+ }
20579
+ async function recordFailure(job, ids, loaded, error, conversationId) {
20580
+ const head = ids[0];
20581
+ if (!head || !loaded.some(Boolean)) return false;
20582
+ if (error instanceof AccountFailureError) {
20583
+ skippedThisSession.add(job.id);
20584
+ return false;
20585
+ }
20586
+ const counted = error instanceof BatchFailureError || error instanceof LockedSourcesError && error.persistentIds.length > 0;
20587
+ if (!counted) return false;
20588
+ const prior = sessionFailures.get(job.id);
20589
+ const count = prior?.head === head ? prior.count + 1 : 1;
20590
+ sessionFailures.set(job.id, { head, count });
20591
+ const terminal = error instanceof BatchFailureError && error.terminal;
20592
+ if (!terminal && count < MAX_ATTEMPTS) return false;
20593
+ skippedThisSession.add(job.id);
20594
+ const now = clock();
20595
+ const lastCounted = failedAtOf(job, head);
20596
+ if (lastCounted !== void 0 && now - lastCounted < MIN_FAILED_SESSION_GAP_MS) return false;
20597
+ const failures = failedSessionsOf(job, head) + 1;
20598
+ if (failures < MAX_FAILED_SESSIONS) {
20599
+ await database.write(
20600
+ () => job.update((r) => {
20601
+ r._setRaw("failed_sessions", failures);
20602
+ r._setRaw("failed_head", head);
20603
+ r._setRaw("failed_at", now);
20604
+ })
20605
+ );
20606
+ return false;
20607
+ }
20608
+ if (error instanceof LockedSourcesError) {
20609
+ const dropped = new Set(error.persistentIds);
20610
+ let newest;
20611
+ for (const message of loaded)
20612
+ if (message && dropped.has(message.uniqueId) && message.messageId > (newest?.seq ?? 0))
20613
+ newest = { id: message.uniqueId, seq: message.messageId };
20614
+ const advance = newest !== void 0 && newest.seq > (seqOf(job) ?? 0) ? newest : void 0;
20615
+ await database.write(
20616
+ () => job.update((r) => {
20617
+ r._setRaw("message_ids", JSON.stringify(idsOf(job).filter((id) => !dropped.has(id))));
20618
+ if (advance) {
20619
+ r._setRaw("watermark", advance.id);
20620
+ r._setRaw("watermark_seq", advance.seq);
20621
+ }
20622
+ r._setRaw("failed_sessions", advance ? MAX_FAILED_SESSIONS : null);
20623
+ r._setRaw("failed_head", advance ? advance.id : null);
20624
+ r._setRaw("failed_at", null);
20625
+ })
20626
+ );
20627
+ attempts.delete(job.id);
20628
+ sessionFailures.delete(job.id);
20629
+ skippedThisSession.delete(job.id);
20630
+ reportError(
20631
+ new Error(
20632
+ `Dropped ${dropped.size} source id(s) that stayed locked for ${failures} sessions to unblock extraction`
20633
+ ),
20634
+ conversationId
20635
+ );
20636
+ return true;
20637
+ }
20638
+ await acknowledge(job, ids, loaded, true);
20639
+ reportError(
20640
+ new Error(
20641
+ `Abandoned an extraction batch of ${ids.length} source id(s) after it failed in ${failures} sessions`
20642
+ ),
20643
+ conversationId
20644
+ );
20645
+ return true;
20646
+ }
20476
20647
  async function drain() {
20477
20648
  if (disposed || running) return;
20478
20649
  running = true;
@@ -20487,21 +20658,23 @@ function createDurableAutoExtractor(options) {
20487
20658
  for (const job of pending) {
20488
20659
  if (disposed) break;
20489
20660
  const attempted = attempts.get(job.id) ?? 0;
20490
- if (attempted >= MAX_ATTEMPTS) continue;
20661
+ if (attempted >= MAX_ATTEMPTS || skippedThisSession.has(job.id)) continue;
20491
20662
  if (processed >= MAX_JOBS_PER_PASS) {
20492
20663
  scheduleAfterSuccess = true;
20493
20664
  break;
20494
20665
  }
20495
20666
  processed++;
20496
20667
  const conversationId = String(job._getRaw("conversation_id"));
20668
+ let ids = [];
20669
+ let loaded = [];
20497
20670
  try {
20498
20671
  const conversations = await storage.conversationsCollection.query(import_watermelondb13.Q.where("conversation_id", conversationId), import_watermelondb13.Q.where("is_deleted", false)).fetchCount();
20499
20672
  if (!conversations) {
20500
20673
  await database.write(() => job.destroyPermanently());
20501
20674
  continue;
20502
20675
  }
20503
- const ids = idsOf(job).slice(0, 20);
20504
- const loaded = await Promise.all(ids.map((id) => getMessageOp(storage, id)));
20676
+ ids = idsOf(job).slice(0, 20);
20677
+ loaded = await Promise.all(ids.map((id) => getMessageOp(storage, id)));
20505
20678
  const missing = ids.filter((_id, index) => loaded[index] === null);
20506
20679
  if (missing.length) {
20507
20680
  if (attempted < MAX_ATTEMPTS - 1)
@@ -20525,17 +20698,21 @@ function createDurableAutoExtractor(options) {
20525
20698
  );
20526
20699
  continue;
20527
20700
  }
20528
- if (loaded.some(
20529
- (message) => message && (message.decryptionStatus || isEncrypted(message.content))
20530
- )) {
20531
- throw new Error("Source messages are locked; retained for retry");
20701
+ const locked = loaded.filter((message) => message && isEncrypted(message.content));
20702
+ if (locked.length) {
20703
+ throw new LockedSourcesError(
20704
+ locked.flatMap(
20705
+ (message) => message.decryptionStatus === "auth_mismatch" || message.decryptionStatus === "invalid_payload" ? [message.uniqueId] : []
20706
+ )
20707
+ );
20532
20708
  }
20533
20709
  const messages = loaded.flatMap(
20534
20710
  (m) => m && m.conversationId === conversationId && (m.role === "user" || m.role === "assistant") ? [{ id: m.uniqueId, role: m.role, content: m.content }] : []
20535
20711
  );
20536
20712
  let success = messages.length === 0;
20713
+ let result;
20537
20714
  if (messages.length) {
20538
- const result = await extractBatch(
20715
+ result = await extractBatch(
20539
20716
  messages,
20540
20717
  conversationId,
20541
20718
  ids,
@@ -20545,41 +20722,35 @@ function createDurableAutoExtractor(options) {
20545
20722
  success = result.failedCount === 0 && result.outcome !== "empty-after-retry";
20546
20723
  options.onTurnComplete?.(result);
20547
20724
  }
20548
- if (!success) throw new Error("Extraction batch incomplete; retained for retry");
20549
- attempts.delete(job.id);
20550
- const sequences = /* @__PURE__ */ new Map();
20551
- for (const message of loaded)
20552
- if (message && message.messageId > 0)
20553
- sequences.set(message.uniqueId, message.messageId);
20554
- await database.write(async () => {
20555
- const current = idsOf(job);
20556
- const acknowledged = ids.filter((id) => current.includes(id));
20557
- if (!acknowledged.length) return;
20558
- const done = new Set(acknowledged);
20559
- const remaining = current.filter((id) => !done.has(id));
20560
- if (remaining.length) {
20561
- await job.update(
20562
- (r) => r._setRaw("message_ids", JSON.stringify([...acknowledged.slice(-2), ...remaining]))
20563
- );
20564
- } else await job.update((r) => r._setRaw("message_ids", "[]"));
20565
- const newest = acknowledged[acknowledged.length - 1];
20566
- await job.update((r) => {
20567
- r._setRaw("watermark", newest);
20568
- const sequence = sequences.get(newest);
20569
- if (sequence !== void 0) {
20570
- const prior = seqOf(job);
20571
- r._setRaw(
20572
- "watermark_seq",
20573
- prior === void 0 ? sequence : Math.max(prior, sequence)
20725
+ if (!success) {
20726
+ const failure = result?.failure;
20727
+ if (failure?.reason === "http-terminal") {
20728
+ const status = failure.httpStatus;
20729
+ if (status !== void 0 && REQUEST_REJECTED_STATUSES.has(status))
20730
+ throw new BatchFailureError(
20731
+ `Extraction rejected the batch (HTTP ${status}); not retried this session`,
20732
+ true
20574
20733
  );
20575
- }
20576
- });
20577
- });
20734
+ throw new AccountFailureError(
20735
+ `Extraction refused for the account (HTTP ${status ?? "unknown"}); retried next session`
20736
+ );
20737
+ }
20738
+ if ((result?.failedCount ?? 0) > 0 || failure !== void 0 && CONTENT_FAILURE_REASONS.has(failure.reason))
20739
+ throw new BatchFailureError("Extraction batch incomplete; retained for retry", false);
20740
+ throw new Error("Extraction batch incomplete; retained for retry");
20741
+ }
20742
+ await acknowledge(job, ids, loaded, false);
20578
20743
  scheduleAfterSuccess = true;
20579
20744
  } catch (error) {
20580
20745
  attempts.set(job.id, attempted + 1);
20581
20746
  retry = true;
20582
20747
  reportError(error, conversationId);
20748
+ try {
20749
+ if (await recordFailure(job, ids, loaded, error, conversationId))
20750
+ scheduleAfterSuccess = true;
20751
+ } catch (recordError) {
20752
+ reportError(recordError, conversationId);
20753
+ }
20583
20754
  }
20584
20755
  }
20585
20756
  } catch (error) {
@@ -20649,7 +20820,7 @@ function createDurableAutoExtractor(options) {
20649
20820
  const start = boundary >= 0 ? boundary + 1 : Math.max(0, ids.length - Math.max(1, options.windowSize ?? 6));
20650
20821
  if (start >= ids.length) return;
20651
20822
  const ownWatermark = job?._getRaw("watermark");
20652
- const atOwnWatermark = !!job && boundary >= 0 && (ids[boundary] === ownWatermark || sequences.get(ids[boundary]) !== void 0 && sequences.get(ids[boundary]) === seqOf(job));
20823
+ 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));
20653
20824
  const overlapStart = atOwnWatermark ? Math.max(0, boundary - 1) : start;
20654
20825
  const fresh = ids.slice(overlapStart).filter((id) => !foreignSources.has(id));
20655
20826
  if (!fresh.length) return;
@@ -21096,6 +21267,9 @@ var MANUAL_FACT_TYPES = [
21096
21267
  function normalizeManualFactType(value) {
21097
21268
  return typeof value === "string" && MANUAL_FACT_TYPES.includes(value) ? value : void 0;
21098
21269
  }
21270
+ function isSharedScope(scope) {
21271
+ return scope === "shared";
21272
+ }
21099
21273
  async function notifyWritten(options, input, outcome) {
21100
21274
  try {
21101
21275
  await options?.onWritten?.({ input, outcome });
@@ -21164,14 +21338,17 @@ function createMemoryVaultTool(vaultCtx, options, embeddingOptions, cache) {
21164
21338
  try {
21165
21339
  const isUpdate = !!id;
21166
21340
  let previousContent;
21341
+ const scope = options?.scope ?? "private";
21167
21342
  if (isUpdate) {
21168
21343
  const existing = await getVaultMemoryOp(vaultCtx, id);
21169
21344
  if (!existing) {
21170
21345
  return `Error: Memory with ID "${id}" not found. Creating a new memory instead would require a separate call without an ID.`;
21171
21346
  }
21347
+ if (isSharedScope(existing.scope) !== isSharedScope(scope)) {
21348
+ 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.`;
21349
+ }
21172
21350
  previousContent = existing.content;
21173
21351
  }
21174
- const scope = options?.scope ?? "private";
21175
21352
  const operation = {
21176
21353
  action: isUpdate ? "update" : "add",
21177
21354
  content,
@@ -25418,7 +25595,7 @@ __decorateClass([
25418
25595
  ], VaultFolder.prototype, "context", 2);
25419
25596
 
25420
25597
  // src/lib/db/schema.ts
25421
- var SDK_SCHEMA_VERSION = 46;
25598
+ var SDK_SCHEMA_VERSION = 47;
25422
25599
  var sdkSchema = (0, import_watermelondb22.appSchema)({
25423
25600
  version: SDK_SCHEMA_VERSION,
25424
25601
  tables: [
@@ -25434,7 +25611,11 @@ var sdkSchema = (0, import_watermelondb22.appSchema)({
25434
25611
  // source. `watermark` alone is an id, and deleting that message erased
25435
25612
  // the anchor — see the column note in extractionJobs/models.ts.
25436
25613
  { name: "watermark_seq", type: "number", isOptional: true },
25437
- { name: "folder_id", type: "string", isOptional: true }
25614
+ { name: "folder_id", type: "string", isOptional: true },
25615
+ // Poison accounting for the head batch; see extractionJobs/models.ts.
25616
+ { name: "failed_sessions", type: "number", isOptional: true },
25617
+ { name: "failed_head", type: "string", isOptional: true },
25618
+ { name: "failed_at", type: "number", isOptional: true }
25438
25619
  ]
25439
25620
  }),
25440
25621
  // Chat storage tables
@@ -26458,6 +26639,22 @@ var sdkMigrations = (0, import_migrations2.schemaMigrations)({
26458
26639
  ]
26459
26640
  })
26460
26641
  ]
26642
+ },
26643
+ // v46 -> v47: persisted poison count (and when it was last counted) for a
26644
+ // durable-extraction job's head batch. NULL = never failed, which is right
26645
+ // for every existing row.
26646
+ {
26647
+ toVersion: 47,
26648
+ steps: [
26649
+ (0, import_migrations2.addColumns)({
26650
+ table: "memory_extraction_jobs",
26651
+ columns: [
26652
+ { name: "failed_sessions", type: "number", isOptional: true },
26653
+ { name: "failed_head", type: "string", isOptional: true },
26654
+ { name: "failed_at", type: "number", isOptional: true }
26655
+ ]
26656
+ })
26657
+ ]
26461
26658
  }
26462
26659
  ]
26463
26660
  });