@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.
@@ -11419,8 +11419,42 @@ async function vaultMemoryToStored(memory, walletAddress, signMessage, embeddedW
11419
11419
  }
11420
11420
  return raw;
11421
11421
  }
11422
+ function populateNewVaultMemory(record, ctx, opts, encryptedContent) {
11423
+ record._setRaw("content", encryptedContent);
11424
+ record._setRaw("scope", opts.scope ?? "private");
11425
+ record._setRaw("folder_id", opts.folderId ?? null);
11426
+ record._setRaw("user_id", ctx.userId ?? null);
11427
+ record._setRaw("is_deleted", false);
11428
+ if (opts.embedding !== void 0) {
11429
+ record._setRaw("embedding", opts.embedding);
11430
+ record._setRaw("embedding_model", opts.embeddingModel ?? null);
11431
+ }
11432
+ if (opts.sourceChunkIds !== void 0) {
11433
+ record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
11434
+ }
11435
+ record._setRaw("proof_count", opts.proofCount ?? 1);
11436
+ record._setRaw("source", opts.source ?? "manual");
11437
+ if (opts.eventTime) {
11438
+ record._setRaw("event_time_start", opts.eventTime.start ?? null);
11439
+ record._setRaw("event_time_end", opts.eventTime.end ?? null);
11440
+ record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
11441
+ }
11442
+ if (opts.factType !== void 0) {
11443
+ record._setRaw("fact_type", opts.factType);
11444
+ }
11445
+ if (opts.trustTier !== void 0) {
11446
+ record._setRaw("trust_tier", normalizeTrustTier(opts.trustTier));
11447
+ }
11448
+ record._setRaw("visibility", opts.visibility ?? "private");
11449
+ record._setRaw(
11450
+ "published_at",
11451
+ opts.visibility && opts.visibility !== "private" ? opts.publishedAt ?? Date.now() : null
11452
+ );
11453
+ if (opts.geohash !== void 0) {
11454
+ record._setRaw("geohash", opts.geohash);
11455
+ }
11456
+ }
11422
11457
  async function createVaultMemoryOp(ctx, opts) {
11423
- const scope = opts.scope ?? "private";
11424
11458
  const encryptedContent = ctx.walletAddress && ctx.signMessage ? await encryptVaultMemoryContent(
11425
11459
  opts.content,
11426
11460
  ctx.walletAddress,
@@ -11430,47 +11464,14 @@ async function createVaultMemoryOp(ctx, opts) {
11430
11464
  const created = await ctx.database.write(async () => {
11431
11465
  if (ctx.canWrite && !await ctx.canWrite())
11432
11466
  throw new Error("Memory source is no longer eligible");
11433
- return ctx.vaultMemoryCollection.create((record) => {
11434
- record._setRaw("content", encryptedContent);
11435
- record._setRaw("scope", scope);
11436
- record._setRaw("folder_id", opts.folderId ?? null);
11437
- record._setRaw("user_id", ctx.userId ?? null);
11438
- record._setRaw("is_deleted", false);
11439
- if (opts.embedding !== void 0) {
11440
- record._setRaw("embedding", opts.embedding);
11441
- record._setRaw("embedding_model", opts.embeddingModel ?? null);
11442
- }
11443
- if (opts.sourceChunkIds !== void 0) {
11444
- record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
11445
- }
11446
- record._setRaw("proof_count", opts.proofCount ?? 1);
11447
- record._setRaw("source", opts.source ?? "manual");
11448
- if (opts.eventTime) {
11449
- record._setRaw("event_time_start", opts.eventTime.start ?? null);
11450
- record._setRaw("event_time_end", opts.eventTime.end ?? null);
11451
- record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
11452
- }
11453
- if (opts.factType !== void 0) {
11454
- record._setRaw("fact_type", opts.factType);
11455
- }
11456
- if (opts.trustTier !== void 0) {
11457
- record._setRaw("trust_tier", normalizeTrustTier(opts.trustTier));
11458
- }
11459
- record._setRaw("visibility", opts.visibility ?? "private");
11460
- record._setRaw(
11461
- "published_at",
11462
- opts.visibility && opts.visibility !== "private" ? opts.publishedAt ?? Date.now() : null
11463
- );
11464
- if (opts.geohash !== void 0) {
11465
- record._setRaw("geohash", opts.geohash);
11466
- }
11467
- });
11467
+ return ctx.vaultMemoryCollection.create(
11468
+ (record) => populateNewVaultMemory(record, ctx, opts, encryptedContent)
11469
+ );
11468
11470
  });
11469
11471
  return vaultMemoryToStored(created, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner);
11470
11472
  }
11471
11473
  async function createSupersedingMemoryOp(ctx, opts, targetId) {
11472
11474
  if (!targetId) return { created: null, retired: false };
11473
- const scope = opts.scope ?? "private";
11474
11475
  const encryptedContent = ctx.walletAddress && ctx.signMessage ? await encryptVaultMemoryContent(
11475
11476
  opts.content,
11476
11477
  ctx.walletAddress,
@@ -11488,27 +11489,9 @@ async function createSupersedingMemoryOp(ctx, opts, targetId) {
11488
11489
  return;
11489
11490
  }
11490
11491
  if (target.isDeleted || target.supersededBy || !isOwnedByCtxUser(ctx, target)) return;
11491
- createdRecord = await ctx.vaultMemoryCollection.create((record) => {
11492
- record._setRaw("content", encryptedContent);
11493
- record._setRaw("scope", scope);
11494
- record._setRaw("folder_id", opts.folderId ?? null);
11495
- record._setRaw("user_id", ctx.userId ?? null);
11496
- record._setRaw("is_deleted", false);
11497
- if (opts.embedding !== void 0) {
11498
- record._setRaw("embedding", opts.embedding);
11499
- record._setRaw("embedding_model", opts.embeddingModel ?? null);
11500
- }
11501
- if (opts.sourceChunkIds !== void 0) {
11502
- record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
11503
- }
11504
- record._setRaw("proof_count", opts.proofCount ?? 1);
11505
- record._setRaw("source", opts.source ?? "manual");
11506
- if (opts.eventTime) {
11507
- record._setRaw("event_time_start", opts.eventTime.start ?? null);
11508
- record._setRaw("event_time_end", opts.eventTime.end ?? null);
11509
- record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
11510
- }
11511
- });
11492
+ createdRecord = await ctx.vaultMemoryCollection.create(
11493
+ (record) => populateNewVaultMemory(record, ctx, opts, encryptedContent)
11494
+ );
11512
11495
  await target.update((r) => {
11513
11496
  r._setRaw("superseded_by", createdRecord.id);
11514
11497
  r._setRaw("superseded_at", Date.now());
@@ -11588,41 +11571,9 @@ async function createVaultMemoriesBatchOp(ctx, optionsArray) {
11588
11571
  if (ctx.canWrite && !await ctx.canWrite())
11589
11572
  throw new Error("Memory source is no longer eligible");
11590
11573
  const prepared = optionsArray.map(
11591
- (opts, i) => ctx.vaultMemoryCollection.prepareCreate((record) => {
11592
- record._setRaw("content", encryptedContents[i]);
11593
- record._setRaw("scope", opts.scope ?? "private");
11594
- record._setRaw("folder_id", opts.folderId ?? null);
11595
- record._setRaw("user_id", ctx.userId ?? null);
11596
- record._setRaw("is_deleted", false);
11597
- if (optionsArray[i].embedding !== void 0) {
11598
- record._setRaw("embedding", optionsArray[i].embedding);
11599
- record._setRaw("embedding_model", optionsArray[i].embeddingModel ?? null);
11600
- }
11601
- if (opts.sourceChunkIds !== void 0) {
11602
- record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
11603
- }
11604
- record._setRaw("proof_count", opts.proofCount ?? 1);
11605
- record._setRaw("source", opts.source ?? "manual");
11606
- if (opts.eventTime) {
11607
- record._setRaw("event_time_start", opts.eventTime.start ?? null);
11608
- record._setRaw("event_time_end", opts.eventTime.end ?? null);
11609
- record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
11610
- }
11611
- if (opts.factType !== void 0) {
11612
- record._setRaw("fact_type", opts.factType);
11613
- }
11614
- if (opts.trustTier !== void 0) {
11615
- record._setRaw("trust_tier", normalizeTrustTier(opts.trustTier));
11616
- }
11617
- record._setRaw("visibility", opts.visibility ?? "private");
11618
- record._setRaw(
11619
- "published_at",
11620
- opts.visibility && opts.visibility !== "private" ? opts.publishedAt ?? Date.now() : null
11621
- );
11622
- if (opts.geohash !== void 0) {
11623
- record._setRaw("geohash", opts.geohash);
11624
- }
11625
- })
11574
+ (opts, i) => ctx.vaultMemoryCollection.prepareCreate(
11575
+ (record) => populateNewVaultMemory(record, ctx, opts, encryptedContents[i])
11576
+ )
11626
11577
  );
11627
11578
  await ctx.database.batch(...prepared);
11628
11579
  return prepared;
@@ -12443,21 +12394,73 @@ async function hardDeleteDecayedOp(ctx, id, opts) {
12443
12394
  return false;
12444
12395
  }
12445
12396
  }
12446
- async function updateVaultMemoryEmbeddingOp(ctx, id, embedding, embeddingModel) {
12397
+ async function updateVaultMemoryEmbeddingOp(ctx, id, embedding, embeddingModel, expected) {
12447
12398
  try {
12448
12399
  const record = await ctx.vaultMemoryCollection.find(id);
12449
12400
  if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) return false;
12401
+ const readUpdatedAt = record.updatedAt.getTime();
12402
+ const readStoredContent = record._getRaw("content");
12403
+ if (expected?.updatedAt !== void 0 && expected.updatedAt !== readUpdatedAt) return false;
12404
+ if (expected?.content !== void 0) {
12405
+ const current = await vaultMemoryToStored(
12406
+ record,
12407
+ ctx.walletAddress,
12408
+ ctx.signMessage,
12409
+ ctx.embeddedWalletSigner
12410
+ );
12411
+ if (current.content !== expected.content) return false;
12412
+ }
12413
+ let written = false;
12450
12414
  await ctx.database.write(async () => {
12415
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) return;
12416
+ const originalUpdatedAt = record.updatedAt.getTime();
12417
+ if (originalUpdatedAt !== readUpdatedAt || record._getRaw("content") !== readStoredContent)
12418
+ return;
12451
12419
  await record.update((r) => {
12452
12420
  r._setRaw("embedding", embedding);
12453
12421
  r._setRaw("embedding_model", embeddingModel);
12422
+ r._setRaw("updated_at", originalUpdatedAt);
12454
12423
  });
12424
+ written = true;
12455
12425
  });
12456
- return true;
12426
+ return written;
12457
12427
  } catch {
12458
12428
  return false;
12459
12429
  }
12460
12430
  }
12431
+ function normalizeForDedupe(content) {
12432
+ return content.normalize("NFKC").trim().replace(/\s+/g, " ").toLowerCase();
12433
+ }
12434
+ async function findQuarantinedDuplicateOp(ctx, content, sourceIds, where) {
12435
+ const wanted = [...new Set(sourceIds)].sort().join("\0");
12436
+ const target = normalizeForDedupe(content);
12437
+ const rows = await ctx.vaultMemoryCollection.query(
12438
+ import_watermelondb10.Q.where("trust_tier", "quarantined"),
12439
+ import_watermelondb10.Q.where("is_deleted", false),
12440
+ import_watermelondb10.Q.where("scope", where.scope),
12441
+ import_watermelondb10.Q.where("folder_id", where.folderId),
12442
+ ...ctx.userId !== void 0 ? [import_watermelondb10.Q.where("user_id", ctx.userId)] : []
12443
+ ).unsafeFetchRaw();
12444
+ for (const raw of rows) {
12445
+ let ids;
12446
+ try {
12447
+ ids = JSON.parse(raw.source_chunk_ids ?? "[]");
12448
+ } catch {
12449
+ continue;
12450
+ }
12451
+ if (!Array.isArray(ids)) continue;
12452
+ const key = [...new Set(ids.filter((id) => typeof id === "string"))].sort().join("\0");
12453
+ if (key !== wanted) continue;
12454
+ const stored = await vaultMemoryRawToStored(
12455
+ raw,
12456
+ ctx.walletAddress,
12457
+ ctx.signMessage,
12458
+ ctx.embeddedWalletSigner
12459
+ );
12460
+ if (normalizeForDedupe(stored.content) === target) return stored.uniqueId;
12461
+ }
12462
+ return null;
12463
+ }
12461
12464
 
12462
12465
  // src/lib/memory/decay.ts
12463
12466
  init_logger();
@@ -12482,6 +12485,10 @@ var DEFAULT_DECAY_POLICY = {
12482
12485
  hardDeleteWindowMs: HARD_DELETE_WINDOW_MS,
12483
12486
  pastEventGraceMs: PAST_EVENT_GRACE_MS
12484
12487
  };
12488
+ function lastActivityAt(m) {
12489
+ const observedAt = Number.isFinite(m.lastObservedAt) ? m.lastObservedAt : m.updatedAt;
12490
+ return Math.max(m.updatedAt, observedAt);
12491
+ }
12485
12492
  function resolvePolicy(policy) {
12486
12493
  if (!policy) return DEFAULT_DECAY_POLICY;
12487
12494
  return {
@@ -12521,8 +12528,7 @@ function classifyDecay(m, now, policy) {
12521
12528
  if (m.eventTimeEnd < now - resolved.pastEventGraceMs) return "archive";
12522
12529
  if (m.eventTimeEnd >= now) return "keep";
12523
12530
  }
12524
- const observedAt = Number.isFinite(m.lastObservedAt) ? m.lastObservedAt : m.updatedAt;
12525
- if (now - Math.max(m.updatedAt, observedAt) > ttlForType(m.factType, resolved)) return "archive";
12531
+ if (now - lastActivityAt(m) > ttlForType(m.factType, resolved)) return "archive";
12526
12532
  return "keep";
12527
12533
  }
12528
12534
 
@@ -15885,6 +15891,24 @@ async function extractAndRetain(messages, retainCtx, options) {
15885
15891
  const tRetain = Date.now();
15886
15892
  for (const { candidate, isQuarantined, reason, signature } of toRetain) {
15887
15893
  try {
15894
+ if (isQuarantined) {
15895
+ const existingId = await findQuarantinedDuplicateOp(
15896
+ retainCtx.vaultCtx,
15897
+ candidate.content,
15898
+ candidate.sourceMessageIds,
15899
+ // The same defaults retain() writes the row with.
15900
+ { scope: options.scope ?? "private", folderId: options.folderId ?? null }
15901
+ ).catch(() => null);
15902
+ if (existingId) {
15903
+ quarantinedInfo.push({
15904
+ candidate,
15905
+ memoryId: existingId,
15906
+ reason,
15907
+ signature
15908
+ });
15909
+ continue;
15910
+ }
15911
+ }
15888
15912
  const result = await retain(candidate.content, retainCtx, {
15889
15913
  source: "auto-extracted",
15890
15914
  sourceChunkIds: candidate.sourceMessageIds,
@@ -16335,7 +16359,7 @@ function createLlmDecayClassifier(options) {
16335
16359
  if (!content || content.trim().length === 0) return ruleVerdict;
16336
16360
  const trimmed = content.trim().slice(0, MAX_CONTENT_CHARS);
16337
16361
  const safe = redactor ? (await redactor.redactTextAsync(trimmed)).text : trimmed;
16338
- const meta = `factType: ${input.factType ?? "none"}; ageDays: ${ageDays(input.updatedAt, now)}`;
16362
+ const meta = `factType: ${input.factType ?? "none"}; ageDays: ${ageDays(lastActivityAt(input), now)}`;
16339
16363
  let parsed;
16340
16364
  try {
16341
16365
  parsed = await callPortalJsonCompletion({
@@ -16368,9 +16392,9 @@ Should this be kept or archived?`,
16368
16392
  }
16369
16393
  };
16370
16394
  }
16371
- function ageDays(updatedAt, now) {
16372
- if (!Number.isFinite(updatedAt) || !Number.isFinite(now)) return 0;
16373
- return Math.max(0, Math.floor((now - updatedAt) / (24 * 60 * 60 * 1e3)));
16395
+ function ageDays(activityAt, now) {
16396
+ if (!Number.isFinite(activityAt) || !Number.isFinite(now)) return 0;
16397
+ return Math.max(0, Math.floor((now - activityAt) / (24 * 60 * 60 * 1e3)));
16374
16398
  }
16375
16399
  function parseVerdict(parsed) {
16376
16400
  if (typeof parsed !== "object" || parsed === null) return null;
@@ -16424,7 +16448,7 @@ function createDecaySweeper(options) {
16424
16448
  if (!classifier || !isBorderline(input)) return ruleVerdict;
16425
16449
  if (input.id) {
16426
16450
  const cached = classifierCache.get(input.id);
16427
- if (cached && cached.updatedAt === input.updatedAt) return cached.verdict;
16451
+ if (cached && cached.activityAt === lastActivityAt(input)) return cached.verdict;
16428
16452
  }
16429
16453
  if (sweep.classifierCalls >= maxClassifierCalls) {
16430
16454
  if (!sweep.ceilingLogged) {
@@ -16438,7 +16462,7 @@ function createDecaySweeper(options) {
16438
16462
  sweep.classifierCalls++;
16439
16463
  try {
16440
16464
  const verdict = await classifier.classify(input, ruleVerdict, now);
16441
- if (input.id) classifierCache.set(input.id, { updatedAt: input.updatedAt, verdict });
16465
+ if (input.id) classifierCache.set(input.id, { activityAt: lastActivityAt(input), verdict });
16442
16466
  return verdict;
16443
16467
  } catch (err) {
16444
16468
  getLogger().warn(
@@ -19075,6 +19099,37 @@ async function assembleMemoryContext(options) {
19075
19099
  var import_watermelondb12 = require("@nozbe/watermelondb");
19076
19100
  var MAX_JOBS_PER_PASS = 3;
19077
19101
  var MAX_ATTEMPTS = 3;
19102
+ var MAX_FAILED_SESSIONS = 3;
19103
+ var MIN_FAILED_SESSION_GAP_MS = 60 * 60 * 1e3;
19104
+ var REQUEST_REJECTED_STATUSES = /* @__PURE__ */ new Set([400, 404, 413, 422]);
19105
+ var CONTENT_FAILURE_REASONS = /* @__PURE__ */ new Set([
19106
+ "empty-content",
19107
+ "invalid-json",
19108
+ "null-completion",
19109
+ "body-parse-failed"
19110
+ ]);
19111
+ var BACKOFF_ALLOWANCE_MS = 2100;
19112
+ var RETAIN_BUDGET_MS = 12e4;
19113
+ function defaultBatchTimeoutMs(extract) {
19114
+ const attempts = Math.max(1, extract.maxAttempts ?? 3);
19115
+ const calls = (extract.timeoutMs ?? 6e4) * attempts + BACKOFF_ALLOWANCE_MS * (attempts - 1);
19116
+ const extraction = extract.totalTimeoutMs !== void 0 ? Math.min(extract.totalTimeoutMs, calls) : calls;
19117
+ return extraction + RETAIN_BUDGET_MS;
19118
+ }
19119
+ var BatchFailureError = class extends Error {
19120
+ constructor(message, terminal) {
19121
+ super(message);
19122
+ this.terminal = terminal;
19123
+ }
19124
+ };
19125
+ var AccountFailureError = class extends Error {
19126
+ };
19127
+ var LockedSourcesError = class extends Error {
19128
+ constructor(persistentIds) {
19129
+ super("Source messages are locked; retained for retry");
19130
+ this.persistentIds = persistentIds;
19131
+ }
19132
+ };
19078
19133
  function idsOf(job) {
19079
19134
  return JSON.parse(String(job._getRaw("message_ids")));
19080
19135
  }
@@ -19082,6 +19137,16 @@ function seqOf(job) {
19082
19137
  const raw = job._getRaw("watermark_seq");
19083
19138
  return typeof raw === "number" && raw > 0 ? raw : void 0;
19084
19139
  }
19140
+ function failedSessionsOf(job, head) {
19141
+ if (job._getRaw("failed_head") !== head) return 0;
19142
+ const raw = job._getRaw("failed_sessions");
19143
+ return typeof raw === "number" && raw > 0 ? raw : 0;
19144
+ }
19145
+ function failedAtOf(job, head) {
19146
+ if (job._getRaw("failed_head") !== head) return void 0;
19147
+ const raw = job._getRaw("failed_at");
19148
+ return typeof raw === "number" ? raw : void 0;
19149
+ }
19085
19150
  function createDurableAutoExtractor(options) {
19086
19151
  const vault = options.retainCtx.vaultCtx;
19087
19152
  const database = vault.database;
@@ -19104,6 +19169,9 @@ function createDurableAutoExtractor(options) {
19104
19169
  let writes = Promise.resolve();
19105
19170
  let storeFailures = 0;
19106
19171
  const attempts = /* @__PURE__ */ new Map();
19172
+ const sessionFailures = /* @__PURE__ */ new Map();
19173
+ const skippedThisSession = /* @__PURE__ */ new Set();
19174
+ const clock = options.now ?? Date.now;
19107
19175
  const reportError = (error, conversationId) => {
19108
19176
  try {
19109
19177
  options.onError?.(error instanceof Error ? error : new Error(String(error)), conversationId);
@@ -19132,6 +19200,7 @@ function createDurableAutoExtractor(options) {
19132
19200
  }
19133
19201
  async function extractBatch(messages, conversationId, ids, jobScope, jobFolderId) {
19134
19202
  let watchdog;
19203
+ let cancelled = false;
19135
19204
  const model = options.modelForScope?.(jobScope) ?? options.extract.model;
19136
19205
  try {
19137
19206
  return await Promise.race([
@@ -19142,7 +19211,7 @@ function createDurableAutoExtractor(options) {
19142
19211
  ...options.retainCtx,
19143
19212
  vaultCtx: {
19144
19213
  ...vault,
19145
- canWrite: async () => !disposed && await storage.conversationsCollection.query(import_watermelondb12.Q.where("conversation_id", conversationId), import_watermelondb12.Q.where("is_deleted", false)).fetchCount() > 0 && await storage.messagesCollection.query(import_watermelondb12.Q.where("conversation_id", conversationId), import_watermelondb12.Q.where("id", import_watermelondb12.Q.oneOf(ids))).fetchCount() === ids.length && (!vault.canWrite || await vault.canWrite()) && !disposed
19214
+ canWrite: async () => !disposed && !cancelled && await storage.conversationsCollection.query(import_watermelondb12.Q.where("conversation_id", conversationId), import_watermelondb12.Q.where("is_deleted", false)).fetchCount() > 0 && await storage.messagesCollection.query(import_watermelondb12.Q.where("conversation_id", conversationId), import_watermelondb12.Q.where("id", import_watermelondb12.Q.oneOf(ids))).fetchCount() === ids.length && (!vault.canWrite || await vault.canWrite()) && !disposed && !cancelled
19146
19215
  }
19147
19216
  },
19148
19217
  // The model follows the job's scope, not the mode alive at drain time.
@@ -19162,14 +19231,116 @@ function createDurableAutoExtractor(options) {
19162
19231
  new Promise((_resolve, reject) => {
19163
19232
  watchdog = setTimeout(
19164
19233
  () => reject(new Error("Extraction did not settle in time; retained for retry")),
19165
- options.batchTimeoutMs ?? 18e4
19234
+ options.batchTimeoutMs ?? defaultBatchTimeoutMs(options.extract)
19166
19235
  );
19167
19236
  })
19168
19237
  ]);
19169
19238
  } finally {
19239
+ cancelled = true;
19170
19240
  if (watchdog !== void 0) clearTimeout(watchdog);
19171
19241
  }
19172
19242
  }
19243
+ async function acknowledge(job, ids, loaded, abandoned) {
19244
+ attempts.delete(job.id);
19245
+ sessionFailures.delete(job.id);
19246
+ skippedThisSession.delete(job.id);
19247
+ const sequences = /* @__PURE__ */ new Map();
19248
+ for (const message of loaded)
19249
+ if (message && message.messageId > 0) sequences.set(message.uniqueId, message.messageId);
19250
+ await database.write(async () => {
19251
+ const current = idsOf(job);
19252
+ const acknowledged = ids.filter((id) => current.includes(id));
19253
+ if (!acknowledged.length) return;
19254
+ const done = new Set(acknowledged);
19255
+ const remaining = current.filter((id) => !done.has(id));
19256
+ await job.update((r) => {
19257
+ r._setRaw(
19258
+ "message_ids",
19259
+ JSON.stringify(
19260
+ remaining.length ? [...abandoned ? [] : acknowledged.slice(-2), ...remaining] : []
19261
+ )
19262
+ );
19263
+ const newest = acknowledged[acknowledged.length - 1];
19264
+ r._setRaw("watermark", newest);
19265
+ const sequence = sequences.get(newest);
19266
+ if (sequence !== void 0) {
19267
+ const prior = seqOf(job);
19268
+ r._setRaw("watermark_seq", prior === void 0 ? sequence : Math.max(prior, sequence));
19269
+ }
19270
+ r._setRaw("failed_sessions", abandoned ? MAX_FAILED_SESSIONS : null);
19271
+ r._setRaw("failed_head", abandoned ? newest : null);
19272
+ r._setRaw("failed_at", null);
19273
+ });
19274
+ });
19275
+ }
19276
+ async function recordFailure(job, ids, loaded, error, conversationId) {
19277
+ const head = ids[0];
19278
+ if (!head || !loaded.some(Boolean)) return false;
19279
+ if (error instanceof AccountFailureError) {
19280
+ skippedThisSession.add(job.id);
19281
+ return false;
19282
+ }
19283
+ const counted = error instanceof BatchFailureError || error instanceof LockedSourcesError && error.persistentIds.length > 0;
19284
+ if (!counted) return false;
19285
+ const prior = sessionFailures.get(job.id);
19286
+ const count = prior?.head === head ? prior.count + 1 : 1;
19287
+ sessionFailures.set(job.id, { head, count });
19288
+ const terminal = error instanceof BatchFailureError && error.terminal;
19289
+ if (!terminal && count < MAX_ATTEMPTS) return false;
19290
+ skippedThisSession.add(job.id);
19291
+ const now = clock();
19292
+ const lastCounted = failedAtOf(job, head);
19293
+ if (lastCounted !== void 0 && now - lastCounted < MIN_FAILED_SESSION_GAP_MS) return false;
19294
+ const failures = failedSessionsOf(job, head) + 1;
19295
+ if (failures < MAX_FAILED_SESSIONS) {
19296
+ await database.write(
19297
+ () => job.update((r) => {
19298
+ r._setRaw("failed_sessions", failures);
19299
+ r._setRaw("failed_head", head);
19300
+ r._setRaw("failed_at", now);
19301
+ })
19302
+ );
19303
+ return false;
19304
+ }
19305
+ if (error instanceof LockedSourcesError) {
19306
+ const dropped = new Set(error.persistentIds);
19307
+ let newest;
19308
+ for (const message of loaded)
19309
+ if (message && dropped.has(message.uniqueId) && message.messageId > (newest?.seq ?? 0))
19310
+ newest = { id: message.uniqueId, seq: message.messageId };
19311
+ const advance = newest !== void 0 && newest.seq > (seqOf(job) ?? 0) ? newest : void 0;
19312
+ await database.write(
19313
+ () => job.update((r) => {
19314
+ r._setRaw("message_ids", JSON.stringify(idsOf(job).filter((id) => !dropped.has(id))));
19315
+ if (advance) {
19316
+ r._setRaw("watermark", advance.id);
19317
+ r._setRaw("watermark_seq", advance.seq);
19318
+ }
19319
+ r._setRaw("failed_sessions", advance ? MAX_FAILED_SESSIONS : null);
19320
+ r._setRaw("failed_head", advance ? advance.id : null);
19321
+ r._setRaw("failed_at", null);
19322
+ })
19323
+ );
19324
+ attempts.delete(job.id);
19325
+ sessionFailures.delete(job.id);
19326
+ skippedThisSession.delete(job.id);
19327
+ reportError(
19328
+ new Error(
19329
+ `Dropped ${dropped.size} source id(s) that stayed locked for ${failures} sessions to unblock extraction`
19330
+ ),
19331
+ conversationId
19332
+ );
19333
+ return true;
19334
+ }
19335
+ await acknowledge(job, ids, loaded, true);
19336
+ reportError(
19337
+ new Error(
19338
+ `Abandoned an extraction batch of ${ids.length} source id(s) after it failed in ${failures} sessions`
19339
+ ),
19340
+ conversationId
19341
+ );
19342
+ return true;
19343
+ }
19173
19344
  async function drain() {
19174
19345
  if (disposed || running) return;
19175
19346
  running = true;
@@ -19184,21 +19355,23 @@ function createDurableAutoExtractor(options) {
19184
19355
  for (const job of pending) {
19185
19356
  if (disposed) break;
19186
19357
  const attempted = attempts.get(job.id) ?? 0;
19187
- if (attempted >= MAX_ATTEMPTS) continue;
19358
+ if (attempted >= MAX_ATTEMPTS || skippedThisSession.has(job.id)) continue;
19188
19359
  if (processed >= MAX_JOBS_PER_PASS) {
19189
19360
  scheduleAfterSuccess = true;
19190
19361
  break;
19191
19362
  }
19192
19363
  processed++;
19193
19364
  const conversationId = String(job._getRaw("conversation_id"));
19365
+ let ids = [];
19366
+ let loaded = [];
19194
19367
  try {
19195
19368
  const conversations = await storage.conversationsCollection.query(import_watermelondb12.Q.where("conversation_id", conversationId), import_watermelondb12.Q.where("is_deleted", false)).fetchCount();
19196
19369
  if (!conversations) {
19197
19370
  await database.write(() => job.destroyPermanently());
19198
19371
  continue;
19199
19372
  }
19200
- const ids = idsOf(job).slice(0, 20);
19201
- const loaded = await Promise.all(ids.map((id) => getMessageOp(storage, id)));
19373
+ ids = idsOf(job).slice(0, 20);
19374
+ loaded = await Promise.all(ids.map((id) => getMessageOp(storage, id)));
19202
19375
  const missing = ids.filter((_id, index) => loaded[index] === null);
19203
19376
  if (missing.length) {
19204
19377
  if (attempted < MAX_ATTEMPTS - 1)
@@ -19222,17 +19395,21 @@ function createDurableAutoExtractor(options) {
19222
19395
  );
19223
19396
  continue;
19224
19397
  }
19225
- if (loaded.some(
19226
- (message) => message && (message.decryptionStatus || isEncrypted(message.content))
19227
- )) {
19228
- throw new Error("Source messages are locked; retained for retry");
19398
+ const locked = loaded.filter((message) => message && isEncrypted(message.content));
19399
+ if (locked.length) {
19400
+ throw new LockedSourcesError(
19401
+ locked.flatMap(
19402
+ (message) => message.decryptionStatus === "auth_mismatch" || message.decryptionStatus === "invalid_payload" ? [message.uniqueId] : []
19403
+ )
19404
+ );
19229
19405
  }
19230
19406
  const messages = loaded.flatMap(
19231
19407
  (m) => m && m.conversationId === conversationId && (m.role === "user" || m.role === "assistant") ? [{ id: m.uniqueId, role: m.role, content: m.content }] : []
19232
19408
  );
19233
19409
  let success = messages.length === 0;
19410
+ let result;
19234
19411
  if (messages.length) {
19235
- const result = await extractBatch(
19412
+ result = await extractBatch(
19236
19413
  messages,
19237
19414
  conversationId,
19238
19415
  ids,
@@ -19242,41 +19419,35 @@ function createDurableAutoExtractor(options) {
19242
19419
  success = result.failedCount === 0 && result.outcome !== "empty-after-retry";
19243
19420
  options.onTurnComplete?.(result);
19244
19421
  }
19245
- if (!success) throw new Error("Extraction batch incomplete; retained for retry");
19246
- attempts.delete(job.id);
19247
- const sequences = /* @__PURE__ */ new Map();
19248
- for (const message of loaded)
19249
- if (message && message.messageId > 0)
19250
- sequences.set(message.uniqueId, message.messageId);
19251
- await database.write(async () => {
19252
- const current = idsOf(job);
19253
- const acknowledged = ids.filter((id) => current.includes(id));
19254
- if (!acknowledged.length) return;
19255
- const done = new Set(acknowledged);
19256
- const remaining = current.filter((id) => !done.has(id));
19257
- if (remaining.length) {
19258
- await job.update(
19259
- (r) => r._setRaw("message_ids", JSON.stringify([...acknowledged.slice(-2), ...remaining]))
19260
- );
19261
- } else await job.update((r) => r._setRaw("message_ids", "[]"));
19262
- const newest = acknowledged[acknowledged.length - 1];
19263
- await job.update((r) => {
19264
- r._setRaw("watermark", newest);
19265
- const sequence = sequences.get(newest);
19266
- if (sequence !== void 0) {
19267
- const prior = seqOf(job);
19268
- r._setRaw(
19269
- "watermark_seq",
19270
- prior === void 0 ? sequence : Math.max(prior, sequence)
19422
+ if (!success) {
19423
+ const failure = result?.failure;
19424
+ if (failure?.reason === "http-terminal") {
19425
+ const status = failure.httpStatus;
19426
+ if (status !== void 0 && REQUEST_REJECTED_STATUSES.has(status))
19427
+ throw new BatchFailureError(
19428
+ `Extraction rejected the batch (HTTP ${status}); not retried this session`,
19429
+ true
19271
19430
  );
19272
- }
19273
- });
19274
- });
19431
+ throw new AccountFailureError(
19432
+ `Extraction refused for the account (HTTP ${status ?? "unknown"}); retried next session`
19433
+ );
19434
+ }
19435
+ if ((result?.failedCount ?? 0) > 0 || failure !== void 0 && CONTENT_FAILURE_REASONS.has(failure.reason))
19436
+ throw new BatchFailureError("Extraction batch incomplete; retained for retry", false);
19437
+ throw new Error("Extraction batch incomplete; retained for retry");
19438
+ }
19439
+ await acknowledge(job, ids, loaded, false);
19275
19440
  scheduleAfterSuccess = true;
19276
19441
  } catch (error) {
19277
19442
  attempts.set(job.id, attempted + 1);
19278
19443
  retry = true;
19279
19444
  reportError(error, conversationId);
19445
+ try {
19446
+ if (await recordFailure(job, ids, loaded, error, conversationId))
19447
+ scheduleAfterSuccess = true;
19448
+ } catch (recordError) {
19449
+ reportError(recordError, conversationId);
19450
+ }
19280
19451
  }
19281
19452
  }
19282
19453
  } catch (error) {
@@ -19346,7 +19517,7 @@ function createDurableAutoExtractor(options) {
19346
19517
  const start = boundary >= 0 ? boundary + 1 : Math.max(0, ids.length - Math.max(1, options.windowSize ?? 6));
19347
19518
  if (start >= ids.length) return;
19348
19519
  const ownWatermark = job?._getRaw("watermark");
19349
- const atOwnWatermark = !!job && boundary >= 0 && (ids[boundary] === ownWatermark || sequences.get(ids[boundary]) !== void 0 && sequences.get(ids[boundary]) === seqOf(job));
19520
+ 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));
19350
19521
  const overlapStart = atOwnWatermark ? Math.max(0, boundary - 1) : start;
19351
19522
  const fresh = ids.slice(overlapStart).filter((id) => !foreignSources.has(id));
19352
19523
  if (!fresh.length) return;
@@ -19793,6 +19964,9 @@ var MANUAL_FACT_TYPES = [
19793
19964
  function normalizeManualFactType(value) {
19794
19965
  return typeof value === "string" && MANUAL_FACT_TYPES.includes(value) ? value : void 0;
19795
19966
  }
19967
+ function isSharedScope(scope) {
19968
+ return scope === "shared";
19969
+ }
19796
19970
  async function notifyWritten(options, input, outcome) {
19797
19971
  try {
19798
19972
  await options?.onWritten?.({ input, outcome });
@@ -19861,14 +20035,17 @@ function createMemoryVaultTool(vaultCtx, options, embeddingOptions, cache) {
19861
20035
  try {
19862
20036
  const isUpdate = !!id;
19863
20037
  let previousContent;
20038
+ const scope = options?.scope ?? "private";
19864
20039
  if (isUpdate) {
19865
20040
  const existing = await getVaultMemoryOp(vaultCtx, id);
19866
20041
  if (!existing) {
19867
20042
  return `Error: Memory with ID "${id}" not found. Creating a new memory instead would require a separate call without an ID.`;
19868
20043
  }
20044
+ if (isSharedScope(existing.scope) !== isSharedScope(scope)) {
20045
+ 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.`;
20046
+ }
19869
20047
  previousContent = existing.content;
19870
20048
  }
19871
- const scope = options?.scope ?? "private";
19872
20049
  const operation = {
19873
20050
  action: isUpdate ? "update" : "add",
19874
20051
  content,
@@ -22931,7 +23108,7 @@ __decorateClass([
22931
23108
  ], VaultFolder.prototype, "context", 2);
22932
23109
 
22933
23110
  // src/lib/db/schema.ts
22934
- var SDK_SCHEMA_VERSION = 46;
23111
+ var SDK_SCHEMA_VERSION = 47;
22935
23112
  var sdkSchema = (0, import_watermelondb21.appSchema)({
22936
23113
  version: SDK_SCHEMA_VERSION,
22937
23114
  tables: [
@@ -22947,7 +23124,11 @@ var sdkSchema = (0, import_watermelondb21.appSchema)({
22947
23124
  // source. `watermark` alone is an id, and deleting that message erased
22948
23125
  // the anchor — see the column note in extractionJobs/models.ts.
22949
23126
  { name: "watermark_seq", type: "number", isOptional: true },
22950
- { name: "folder_id", type: "string", isOptional: true }
23127
+ { name: "folder_id", type: "string", isOptional: true },
23128
+ // Poison accounting for the head batch; see extractionJobs/models.ts.
23129
+ { name: "failed_sessions", type: "number", isOptional: true },
23130
+ { name: "failed_head", type: "string", isOptional: true },
23131
+ { name: "failed_at", type: "number", isOptional: true }
22951
23132
  ]
22952
23133
  }),
22953
23134
  // Chat storage tables
@@ -23971,6 +24152,22 @@ var sdkMigrations = (0, import_migrations2.schemaMigrations)({
23971
24152
  ]
23972
24153
  })
23973
24154
  ]
24155
+ },
24156
+ // v46 -> v47: persisted poison count (and when it was last counted) for a
24157
+ // durable-extraction job's head batch. NULL = never failed, which is right
24158
+ // for every existing row.
24159
+ {
24160
+ toVersion: 47,
24161
+ steps: [
24162
+ (0, import_migrations2.addColumns)({
24163
+ table: "memory_extraction_jobs",
24164
+ columns: [
24165
+ { name: "failed_sessions", type: "number", isOptional: true },
24166
+ { name: "failed_head", type: "string", isOptional: true },
24167
+ { name: "failed_at", type: "number", isOptional: true }
24168
+ ]
24169
+ })
24170
+ ]
23974
24171
  }
23975
24172
  ]
23976
24173
  });