@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.
@@ -11159,8 +11159,42 @@ async function vaultMemoryToStored(memory, walletAddress, signMessage, embeddedW
11159
11159
  }
11160
11160
  return raw;
11161
11161
  }
11162
+ function populateNewVaultMemory(record, ctx, opts, encryptedContent) {
11163
+ record._setRaw("content", encryptedContent);
11164
+ record._setRaw("scope", opts.scope ?? "private");
11165
+ record._setRaw("folder_id", opts.folderId ?? null);
11166
+ record._setRaw("user_id", ctx.userId ?? null);
11167
+ record._setRaw("is_deleted", false);
11168
+ if (opts.embedding !== void 0) {
11169
+ record._setRaw("embedding", opts.embedding);
11170
+ record._setRaw("embedding_model", opts.embeddingModel ?? null);
11171
+ }
11172
+ if (opts.sourceChunkIds !== void 0) {
11173
+ record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
11174
+ }
11175
+ record._setRaw("proof_count", opts.proofCount ?? 1);
11176
+ record._setRaw("source", opts.source ?? "manual");
11177
+ if (opts.eventTime) {
11178
+ record._setRaw("event_time_start", opts.eventTime.start ?? null);
11179
+ record._setRaw("event_time_end", opts.eventTime.end ?? null);
11180
+ record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
11181
+ }
11182
+ if (opts.factType !== void 0) {
11183
+ record._setRaw("fact_type", opts.factType);
11184
+ }
11185
+ if (opts.trustTier !== void 0) {
11186
+ record._setRaw("trust_tier", normalizeTrustTier(opts.trustTier));
11187
+ }
11188
+ record._setRaw("visibility", opts.visibility ?? "private");
11189
+ record._setRaw(
11190
+ "published_at",
11191
+ opts.visibility && opts.visibility !== "private" ? opts.publishedAt ?? Date.now() : null
11192
+ );
11193
+ if (opts.geohash !== void 0) {
11194
+ record._setRaw("geohash", opts.geohash);
11195
+ }
11196
+ }
11162
11197
  async function createVaultMemoryOp(ctx, opts) {
11163
- const scope = opts.scope ?? "private";
11164
11198
  const encryptedContent = ctx.walletAddress && ctx.signMessage ? await encryptVaultMemoryContent(
11165
11199
  opts.content,
11166
11200
  ctx.walletAddress,
@@ -11170,47 +11204,14 @@ async function createVaultMemoryOp(ctx, opts) {
11170
11204
  const created = await ctx.database.write(async () => {
11171
11205
  if (ctx.canWrite && !await ctx.canWrite())
11172
11206
  throw new Error("Memory source is no longer eligible");
11173
- return ctx.vaultMemoryCollection.create((record) => {
11174
- record._setRaw("content", encryptedContent);
11175
- record._setRaw("scope", scope);
11176
- record._setRaw("folder_id", opts.folderId ?? null);
11177
- record._setRaw("user_id", ctx.userId ?? null);
11178
- record._setRaw("is_deleted", false);
11179
- if (opts.embedding !== void 0) {
11180
- record._setRaw("embedding", opts.embedding);
11181
- record._setRaw("embedding_model", opts.embeddingModel ?? null);
11182
- }
11183
- if (opts.sourceChunkIds !== void 0) {
11184
- record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
11185
- }
11186
- record._setRaw("proof_count", opts.proofCount ?? 1);
11187
- record._setRaw("source", opts.source ?? "manual");
11188
- if (opts.eventTime) {
11189
- record._setRaw("event_time_start", opts.eventTime.start ?? null);
11190
- record._setRaw("event_time_end", opts.eventTime.end ?? null);
11191
- record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
11192
- }
11193
- if (opts.factType !== void 0) {
11194
- record._setRaw("fact_type", opts.factType);
11195
- }
11196
- if (opts.trustTier !== void 0) {
11197
- record._setRaw("trust_tier", normalizeTrustTier(opts.trustTier));
11198
- }
11199
- record._setRaw("visibility", opts.visibility ?? "private");
11200
- record._setRaw(
11201
- "published_at",
11202
- opts.visibility && opts.visibility !== "private" ? opts.publishedAt ?? Date.now() : null
11203
- );
11204
- if (opts.geohash !== void 0) {
11205
- record._setRaw("geohash", opts.geohash);
11206
- }
11207
- });
11207
+ return ctx.vaultMemoryCollection.create(
11208
+ (record) => populateNewVaultMemory(record, ctx, opts, encryptedContent)
11209
+ );
11208
11210
  });
11209
11211
  return vaultMemoryToStored(created, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner);
11210
11212
  }
11211
11213
  async function createSupersedingMemoryOp(ctx, opts, targetId) {
11212
11214
  if (!targetId) return { created: null, retired: false };
11213
- const scope = opts.scope ?? "private";
11214
11215
  const encryptedContent = ctx.walletAddress && ctx.signMessage ? await encryptVaultMemoryContent(
11215
11216
  opts.content,
11216
11217
  ctx.walletAddress,
@@ -11228,27 +11229,9 @@ async function createSupersedingMemoryOp(ctx, opts, targetId) {
11228
11229
  return;
11229
11230
  }
11230
11231
  if (target.isDeleted || target.supersededBy || !isOwnedByCtxUser(ctx, target)) return;
11231
- createdRecord = await ctx.vaultMemoryCollection.create((record) => {
11232
- record._setRaw("content", encryptedContent);
11233
- record._setRaw("scope", scope);
11234
- record._setRaw("folder_id", opts.folderId ?? null);
11235
- record._setRaw("user_id", ctx.userId ?? null);
11236
- record._setRaw("is_deleted", false);
11237
- if (opts.embedding !== void 0) {
11238
- record._setRaw("embedding", opts.embedding);
11239
- record._setRaw("embedding_model", opts.embeddingModel ?? null);
11240
- }
11241
- if (opts.sourceChunkIds !== void 0) {
11242
- record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
11243
- }
11244
- record._setRaw("proof_count", opts.proofCount ?? 1);
11245
- record._setRaw("source", opts.source ?? "manual");
11246
- if (opts.eventTime) {
11247
- record._setRaw("event_time_start", opts.eventTime.start ?? null);
11248
- record._setRaw("event_time_end", opts.eventTime.end ?? null);
11249
- record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
11250
- }
11251
- });
11232
+ createdRecord = await ctx.vaultMemoryCollection.create(
11233
+ (record) => populateNewVaultMemory(record, ctx, opts, encryptedContent)
11234
+ );
11252
11235
  await target.update((r) => {
11253
11236
  r._setRaw("superseded_by", createdRecord.id);
11254
11237
  r._setRaw("superseded_at", Date.now());
@@ -11328,41 +11311,9 @@ async function createVaultMemoriesBatchOp(ctx, optionsArray) {
11328
11311
  if (ctx.canWrite && !await ctx.canWrite())
11329
11312
  throw new Error("Memory source is no longer eligible");
11330
11313
  const prepared = optionsArray.map(
11331
- (opts, i) => ctx.vaultMemoryCollection.prepareCreate((record) => {
11332
- record._setRaw("content", encryptedContents[i]);
11333
- record._setRaw("scope", opts.scope ?? "private");
11334
- record._setRaw("folder_id", opts.folderId ?? null);
11335
- record._setRaw("user_id", ctx.userId ?? null);
11336
- record._setRaw("is_deleted", false);
11337
- if (optionsArray[i].embedding !== void 0) {
11338
- record._setRaw("embedding", optionsArray[i].embedding);
11339
- record._setRaw("embedding_model", optionsArray[i].embeddingModel ?? null);
11340
- }
11341
- if (opts.sourceChunkIds !== void 0) {
11342
- record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
11343
- }
11344
- record._setRaw("proof_count", opts.proofCount ?? 1);
11345
- record._setRaw("source", opts.source ?? "manual");
11346
- if (opts.eventTime) {
11347
- record._setRaw("event_time_start", opts.eventTime.start ?? null);
11348
- record._setRaw("event_time_end", opts.eventTime.end ?? null);
11349
- record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
11350
- }
11351
- if (opts.factType !== void 0) {
11352
- record._setRaw("fact_type", opts.factType);
11353
- }
11354
- if (opts.trustTier !== void 0) {
11355
- record._setRaw("trust_tier", normalizeTrustTier(opts.trustTier));
11356
- }
11357
- record._setRaw("visibility", opts.visibility ?? "private");
11358
- record._setRaw(
11359
- "published_at",
11360
- opts.visibility && opts.visibility !== "private" ? opts.publishedAt ?? Date.now() : null
11361
- );
11362
- if (opts.geohash !== void 0) {
11363
- record._setRaw("geohash", opts.geohash);
11364
- }
11365
- })
11314
+ (opts, i) => ctx.vaultMemoryCollection.prepareCreate(
11315
+ (record) => populateNewVaultMemory(record, ctx, opts, encryptedContents[i])
11316
+ )
11366
11317
  );
11367
11318
  await ctx.database.batch(...prepared);
11368
11319
  return prepared;
@@ -12228,21 +12179,73 @@ async function hardDeleteDecayedOp(ctx, id, opts) {
12228
12179
  return false;
12229
12180
  }
12230
12181
  }
12231
- async function updateVaultMemoryEmbeddingOp(ctx, id, embedding, embeddingModel) {
12182
+ async function updateVaultMemoryEmbeddingOp(ctx, id, embedding, embeddingModel, expected) {
12232
12183
  try {
12233
12184
  const record = await ctx.vaultMemoryCollection.find(id);
12234
12185
  if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) return false;
12186
+ const readUpdatedAt = record.updatedAt.getTime();
12187
+ const readStoredContent = record._getRaw("content");
12188
+ if (expected?.updatedAt !== void 0 && expected.updatedAt !== readUpdatedAt) return false;
12189
+ if (expected?.content !== void 0) {
12190
+ const current = await vaultMemoryToStored(
12191
+ record,
12192
+ ctx.walletAddress,
12193
+ ctx.signMessage,
12194
+ ctx.embeddedWalletSigner
12195
+ );
12196
+ if (current.content !== expected.content) return false;
12197
+ }
12198
+ let written = false;
12235
12199
  await ctx.database.write(async () => {
12200
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) return;
12201
+ const originalUpdatedAt = record.updatedAt.getTime();
12202
+ if (originalUpdatedAt !== readUpdatedAt || record._getRaw("content") !== readStoredContent)
12203
+ return;
12236
12204
  await record.update((r) => {
12237
12205
  r._setRaw("embedding", embedding);
12238
12206
  r._setRaw("embedding_model", embeddingModel);
12207
+ r._setRaw("updated_at", originalUpdatedAt);
12239
12208
  });
12209
+ written = true;
12240
12210
  });
12241
- return true;
12211
+ return written;
12242
12212
  } catch {
12243
12213
  return false;
12244
12214
  }
12245
12215
  }
12216
+ function normalizeForDedupe(content) {
12217
+ return content.normalize("NFKC").trim().replace(/\s+/g, " ").toLowerCase();
12218
+ }
12219
+ async function findQuarantinedDuplicateOp(ctx, content, sourceIds, where) {
12220
+ const wanted = [...new Set(sourceIds)].sort().join("\0");
12221
+ const target = normalizeForDedupe(content);
12222
+ const rows = await ctx.vaultMemoryCollection.query(
12223
+ Q6.where("trust_tier", "quarantined"),
12224
+ Q6.where("is_deleted", false),
12225
+ Q6.where("scope", where.scope),
12226
+ Q6.where("folder_id", where.folderId),
12227
+ ...ctx.userId !== void 0 ? [Q6.where("user_id", ctx.userId)] : []
12228
+ ).unsafeFetchRaw();
12229
+ for (const raw of rows) {
12230
+ let ids;
12231
+ try {
12232
+ ids = JSON.parse(raw.source_chunk_ids ?? "[]");
12233
+ } catch {
12234
+ continue;
12235
+ }
12236
+ if (!Array.isArray(ids)) continue;
12237
+ const key = [...new Set(ids.filter((id) => typeof id === "string"))].sort().join("\0");
12238
+ if (key !== wanted) continue;
12239
+ const stored = await vaultMemoryRawToStored(
12240
+ raw,
12241
+ ctx.walletAddress,
12242
+ ctx.signMessage,
12243
+ ctx.embeddedWalletSigner
12244
+ );
12245
+ if (normalizeForDedupe(stored.content) === target) return stored.uniqueId;
12246
+ }
12247
+ return null;
12248
+ }
12246
12249
 
12247
12250
  // src/lib/memory/decay.ts
12248
12251
  var DAY_MS = 24 * 60 * 60 * 1e3;
@@ -12266,6 +12269,10 @@ var DEFAULT_DECAY_POLICY = {
12266
12269
  hardDeleteWindowMs: HARD_DELETE_WINDOW_MS,
12267
12270
  pastEventGraceMs: PAST_EVENT_GRACE_MS
12268
12271
  };
12272
+ function lastActivityAt(m) {
12273
+ const observedAt = Number.isFinite(m.lastObservedAt) ? m.lastObservedAt : m.updatedAt;
12274
+ return Math.max(m.updatedAt, observedAt);
12275
+ }
12269
12276
  function resolvePolicy(policy) {
12270
12277
  if (!policy) return DEFAULT_DECAY_POLICY;
12271
12278
  return {
@@ -12305,8 +12312,7 @@ function classifyDecay(m, now, policy) {
12305
12312
  if (m.eventTimeEnd < now - resolved.pastEventGraceMs) return "archive";
12306
12313
  if (m.eventTimeEnd >= now) return "keep";
12307
12314
  }
12308
- const observedAt = Number.isFinite(m.lastObservedAt) ? m.lastObservedAt : m.updatedAt;
12309
- if (now - Math.max(m.updatedAt, observedAt) > ttlForType(m.factType, resolved)) return "archive";
12315
+ if (now - lastActivityAt(m) > ttlForType(m.factType, resolved)) return "archive";
12310
12316
  return "keep";
12311
12317
  }
12312
12318
 
@@ -15307,6 +15313,24 @@ async function extractAndRetain(messages, retainCtx, options) {
15307
15313
  const tRetain = Date.now();
15308
15314
  for (const { candidate, isQuarantined, reason, signature } of toRetain) {
15309
15315
  try {
15316
+ if (isQuarantined) {
15317
+ const existingId = await findQuarantinedDuplicateOp(
15318
+ retainCtx.vaultCtx,
15319
+ candidate.content,
15320
+ candidate.sourceMessageIds,
15321
+ // The same defaults retain() writes the row with.
15322
+ { scope: options.scope ?? "private", folderId: options.folderId ?? null }
15323
+ ).catch(() => null);
15324
+ if (existingId) {
15325
+ quarantinedInfo.push({
15326
+ candidate,
15327
+ memoryId: existingId,
15328
+ reason,
15329
+ signature
15330
+ });
15331
+ continue;
15332
+ }
15333
+ }
15310
15334
  const result = await retain(candidate.content, retainCtx, {
15311
15335
  source: "auto-extracted",
15312
15336
  sourceChunkIds: candidate.sourceMessageIds,
@@ -15752,7 +15776,7 @@ function createLlmDecayClassifier(options) {
15752
15776
  if (!content || content.trim().length === 0) return ruleVerdict;
15753
15777
  const trimmed = content.trim().slice(0, MAX_CONTENT_CHARS);
15754
15778
  const safe = redactor ? (await redactor.redactTextAsync(trimmed)).text : trimmed;
15755
- const meta = `factType: ${input.factType ?? "none"}; ageDays: ${ageDays(input.updatedAt, now)}`;
15779
+ const meta = `factType: ${input.factType ?? "none"}; ageDays: ${ageDays(lastActivityAt(input), now)}`;
15756
15780
  let parsed;
15757
15781
  try {
15758
15782
  parsed = await callPortalJsonCompletion({
@@ -15785,9 +15809,9 @@ Should this be kept or archived?`,
15785
15809
  }
15786
15810
  };
15787
15811
  }
15788
- function ageDays(updatedAt, now) {
15789
- if (!Number.isFinite(updatedAt) || !Number.isFinite(now)) return 0;
15790
- return Math.max(0, Math.floor((now - updatedAt) / (24 * 60 * 60 * 1e3)));
15812
+ function ageDays(activityAt, now) {
15813
+ if (!Number.isFinite(activityAt) || !Number.isFinite(now)) return 0;
15814
+ return Math.max(0, Math.floor((now - activityAt) / (24 * 60 * 60 * 1e3)));
15791
15815
  }
15792
15816
  function parseVerdict(parsed) {
15793
15817
  if (typeof parsed !== "object" || parsed === null) return null;
@@ -15840,7 +15864,7 @@ function createDecaySweeper(options) {
15840
15864
  if (!classifier || !isBorderline(input)) return ruleVerdict;
15841
15865
  if (input.id) {
15842
15866
  const cached = classifierCache.get(input.id);
15843
- if (cached && cached.updatedAt === input.updatedAt) return cached.verdict;
15867
+ if (cached && cached.activityAt === lastActivityAt(input)) return cached.verdict;
15844
15868
  }
15845
15869
  if (sweep.classifierCalls >= maxClassifierCalls) {
15846
15870
  if (!sweep.ceilingLogged) {
@@ -15854,7 +15878,7 @@ function createDecaySweeper(options) {
15854
15878
  sweep.classifierCalls++;
15855
15879
  try {
15856
15880
  const verdict = await classifier.classify(input, ruleVerdict, now);
15857
- if (input.id) classifierCache.set(input.id, { updatedAt: input.updatedAt, verdict });
15881
+ if (input.id) classifierCache.set(input.id, { activityAt: lastActivityAt(input), verdict });
15858
15882
  return verdict;
15859
15883
  } catch (err) {
15860
15884
  getLogger().warn(
@@ -18472,6 +18496,37 @@ async function assembleMemoryContext(options) {
18472
18496
  import { Q as Q8 } from "@nozbe/watermelondb";
18473
18497
  var MAX_JOBS_PER_PASS = 3;
18474
18498
  var MAX_ATTEMPTS = 3;
18499
+ var MAX_FAILED_SESSIONS = 3;
18500
+ var MIN_FAILED_SESSION_GAP_MS = 60 * 60 * 1e3;
18501
+ var REQUEST_REJECTED_STATUSES = /* @__PURE__ */ new Set([400, 404, 413, 422]);
18502
+ var CONTENT_FAILURE_REASONS = /* @__PURE__ */ new Set([
18503
+ "empty-content",
18504
+ "invalid-json",
18505
+ "null-completion",
18506
+ "body-parse-failed"
18507
+ ]);
18508
+ var BACKOFF_ALLOWANCE_MS = 2100;
18509
+ var RETAIN_BUDGET_MS = 12e4;
18510
+ function defaultBatchTimeoutMs(extract) {
18511
+ const attempts = Math.max(1, extract.maxAttempts ?? 3);
18512
+ const calls = (extract.timeoutMs ?? 6e4) * attempts + BACKOFF_ALLOWANCE_MS * (attempts - 1);
18513
+ const extraction = extract.totalTimeoutMs !== void 0 ? Math.min(extract.totalTimeoutMs, calls) : calls;
18514
+ return extraction + RETAIN_BUDGET_MS;
18515
+ }
18516
+ var BatchFailureError = class extends Error {
18517
+ constructor(message, terminal) {
18518
+ super(message);
18519
+ this.terminal = terminal;
18520
+ }
18521
+ };
18522
+ var AccountFailureError = class extends Error {
18523
+ };
18524
+ var LockedSourcesError = class extends Error {
18525
+ constructor(persistentIds) {
18526
+ super("Source messages are locked; retained for retry");
18527
+ this.persistentIds = persistentIds;
18528
+ }
18529
+ };
18475
18530
  function idsOf(job) {
18476
18531
  return JSON.parse(String(job._getRaw("message_ids")));
18477
18532
  }
@@ -18479,6 +18534,16 @@ function seqOf(job) {
18479
18534
  const raw = job._getRaw("watermark_seq");
18480
18535
  return typeof raw === "number" && raw > 0 ? raw : void 0;
18481
18536
  }
18537
+ function failedSessionsOf(job, head) {
18538
+ if (job._getRaw("failed_head") !== head) return 0;
18539
+ const raw = job._getRaw("failed_sessions");
18540
+ return typeof raw === "number" && raw > 0 ? raw : 0;
18541
+ }
18542
+ function failedAtOf(job, head) {
18543
+ if (job._getRaw("failed_head") !== head) return void 0;
18544
+ const raw = job._getRaw("failed_at");
18545
+ return typeof raw === "number" ? raw : void 0;
18546
+ }
18482
18547
  function createDurableAutoExtractor(options) {
18483
18548
  const vault = options.retainCtx.vaultCtx;
18484
18549
  const database = vault.database;
@@ -18501,6 +18566,9 @@ function createDurableAutoExtractor(options) {
18501
18566
  let writes = Promise.resolve();
18502
18567
  let storeFailures = 0;
18503
18568
  const attempts = /* @__PURE__ */ new Map();
18569
+ const sessionFailures = /* @__PURE__ */ new Map();
18570
+ const skippedThisSession = /* @__PURE__ */ new Set();
18571
+ const clock = options.now ?? Date.now;
18504
18572
  const reportError = (error, conversationId) => {
18505
18573
  try {
18506
18574
  options.onError?.(error instanceof Error ? error : new Error(String(error)), conversationId);
@@ -18529,6 +18597,7 @@ function createDurableAutoExtractor(options) {
18529
18597
  }
18530
18598
  async function extractBatch(messages, conversationId, ids, jobScope, jobFolderId) {
18531
18599
  let watchdog;
18600
+ let cancelled = false;
18532
18601
  const model = options.modelForScope?.(jobScope) ?? options.extract.model;
18533
18602
  try {
18534
18603
  return await Promise.race([
@@ -18539,7 +18608,7 @@ function createDurableAutoExtractor(options) {
18539
18608
  ...options.retainCtx,
18540
18609
  vaultCtx: {
18541
18610
  ...vault,
18542
- canWrite: async () => !disposed && await storage.conversationsCollection.query(Q8.where("conversation_id", conversationId), Q8.where("is_deleted", false)).fetchCount() > 0 && await storage.messagesCollection.query(Q8.where("conversation_id", conversationId), Q8.where("id", Q8.oneOf(ids))).fetchCount() === ids.length && (!vault.canWrite || await vault.canWrite()) && !disposed
18611
+ canWrite: async () => !disposed && !cancelled && await storage.conversationsCollection.query(Q8.where("conversation_id", conversationId), Q8.where("is_deleted", false)).fetchCount() > 0 && await storage.messagesCollection.query(Q8.where("conversation_id", conversationId), Q8.where("id", Q8.oneOf(ids))).fetchCount() === ids.length && (!vault.canWrite || await vault.canWrite()) && !disposed && !cancelled
18543
18612
  }
18544
18613
  },
18545
18614
  // The model follows the job's scope, not the mode alive at drain time.
@@ -18559,14 +18628,116 @@ function createDurableAutoExtractor(options) {
18559
18628
  new Promise((_resolve, reject) => {
18560
18629
  watchdog = setTimeout(
18561
18630
  () => reject(new Error("Extraction did not settle in time; retained for retry")),
18562
- options.batchTimeoutMs ?? 18e4
18631
+ options.batchTimeoutMs ?? defaultBatchTimeoutMs(options.extract)
18563
18632
  );
18564
18633
  })
18565
18634
  ]);
18566
18635
  } finally {
18636
+ cancelled = true;
18567
18637
  if (watchdog !== void 0) clearTimeout(watchdog);
18568
18638
  }
18569
18639
  }
18640
+ async function acknowledge(job, ids, loaded, abandoned) {
18641
+ attempts.delete(job.id);
18642
+ sessionFailures.delete(job.id);
18643
+ skippedThisSession.delete(job.id);
18644
+ const sequences = /* @__PURE__ */ new Map();
18645
+ for (const message of loaded)
18646
+ if (message && message.messageId > 0) sequences.set(message.uniqueId, message.messageId);
18647
+ await database.write(async () => {
18648
+ const current = idsOf(job);
18649
+ const acknowledged = ids.filter((id) => current.includes(id));
18650
+ if (!acknowledged.length) return;
18651
+ const done = new Set(acknowledged);
18652
+ const remaining = current.filter((id) => !done.has(id));
18653
+ await job.update((r) => {
18654
+ r._setRaw(
18655
+ "message_ids",
18656
+ JSON.stringify(
18657
+ remaining.length ? [...abandoned ? [] : acknowledged.slice(-2), ...remaining] : []
18658
+ )
18659
+ );
18660
+ const newest = acknowledged[acknowledged.length - 1];
18661
+ r._setRaw("watermark", newest);
18662
+ const sequence = sequences.get(newest);
18663
+ if (sequence !== void 0) {
18664
+ const prior = seqOf(job);
18665
+ r._setRaw("watermark_seq", prior === void 0 ? sequence : Math.max(prior, sequence));
18666
+ }
18667
+ r._setRaw("failed_sessions", abandoned ? MAX_FAILED_SESSIONS : null);
18668
+ r._setRaw("failed_head", abandoned ? newest : null);
18669
+ r._setRaw("failed_at", null);
18670
+ });
18671
+ });
18672
+ }
18673
+ async function recordFailure(job, ids, loaded, error, conversationId) {
18674
+ const head = ids[0];
18675
+ if (!head || !loaded.some(Boolean)) return false;
18676
+ if (error instanceof AccountFailureError) {
18677
+ skippedThisSession.add(job.id);
18678
+ return false;
18679
+ }
18680
+ const counted = error instanceof BatchFailureError || error instanceof LockedSourcesError && error.persistentIds.length > 0;
18681
+ if (!counted) return false;
18682
+ const prior = sessionFailures.get(job.id);
18683
+ const count = prior?.head === head ? prior.count + 1 : 1;
18684
+ sessionFailures.set(job.id, { head, count });
18685
+ const terminal = error instanceof BatchFailureError && error.terminal;
18686
+ if (!terminal && count < MAX_ATTEMPTS) return false;
18687
+ skippedThisSession.add(job.id);
18688
+ const now = clock();
18689
+ const lastCounted = failedAtOf(job, head);
18690
+ if (lastCounted !== void 0 && now - lastCounted < MIN_FAILED_SESSION_GAP_MS) return false;
18691
+ const failures = failedSessionsOf(job, head) + 1;
18692
+ if (failures < MAX_FAILED_SESSIONS) {
18693
+ await database.write(
18694
+ () => job.update((r) => {
18695
+ r._setRaw("failed_sessions", failures);
18696
+ r._setRaw("failed_head", head);
18697
+ r._setRaw("failed_at", now);
18698
+ })
18699
+ );
18700
+ return false;
18701
+ }
18702
+ if (error instanceof LockedSourcesError) {
18703
+ const dropped = new Set(error.persistentIds);
18704
+ let newest;
18705
+ for (const message of loaded)
18706
+ if (message && dropped.has(message.uniqueId) && message.messageId > (newest?.seq ?? 0))
18707
+ newest = { id: message.uniqueId, seq: message.messageId };
18708
+ const advance = newest !== void 0 && newest.seq > (seqOf(job) ?? 0) ? newest : void 0;
18709
+ await database.write(
18710
+ () => job.update((r) => {
18711
+ r._setRaw("message_ids", JSON.stringify(idsOf(job).filter((id) => !dropped.has(id))));
18712
+ if (advance) {
18713
+ r._setRaw("watermark", advance.id);
18714
+ r._setRaw("watermark_seq", advance.seq);
18715
+ }
18716
+ r._setRaw("failed_sessions", advance ? MAX_FAILED_SESSIONS : null);
18717
+ r._setRaw("failed_head", advance ? advance.id : null);
18718
+ r._setRaw("failed_at", null);
18719
+ })
18720
+ );
18721
+ attempts.delete(job.id);
18722
+ sessionFailures.delete(job.id);
18723
+ skippedThisSession.delete(job.id);
18724
+ reportError(
18725
+ new Error(
18726
+ `Dropped ${dropped.size} source id(s) that stayed locked for ${failures} sessions to unblock extraction`
18727
+ ),
18728
+ conversationId
18729
+ );
18730
+ return true;
18731
+ }
18732
+ await acknowledge(job, ids, loaded, true);
18733
+ reportError(
18734
+ new Error(
18735
+ `Abandoned an extraction batch of ${ids.length} source id(s) after it failed in ${failures} sessions`
18736
+ ),
18737
+ conversationId
18738
+ );
18739
+ return true;
18740
+ }
18570
18741
  async function drain() {
18571
18742
  if (disposed || running) return;
18572
18743
  running = true;
@@ -18581,21 +18752,23 @@ function createDurableAutoExtractor(options) {
18581
18752
  for (const job of pending) {
18582
18753
  if (disposed) break;
18583
18754
  const attempted = attempts.get(job.id) ?? 0;
18584
- if (attempted >= MAX_ATTEMPTS) continue;
18755
+ if (attempted >= MAX_ATTEMPTS || skippedThisSession.has(job.id)) continue;
18585
18756
  if (processed >= MAX_JOBS_PER_PASS) {
18586
18757
  scheduleAfterSuccess = true;
18587
18758
  break;
18588
18759
  }
18589
18760
  processed++;
18590
18761
  const conversationId = String(job._getRaw("conversation_id"));
18762
+ let ids = [];
18763
+ let loaded = [];
18591
18764
  try {
18592
18765
  const conversations = await storage.conversationsCollection.query(Q8.where("conversation_id", conversationId), Q8.where("is_deleted", false)).fetchCount();
18593
18766
  if (!conversations) {
18594
18767
  await database.write(() => job.destroyPermanently());
18595
18768
  continue;
18596
18769
  }
18597
- const ids = idsOf(job).slice(0, 20);
18598
- const loaded = await Promise.all(ids.map((id) => getMessageOp(storage, id)));
18770
+ ids = idsOf(job).slice(0, 20);
18771
+ loaded = await Promise.all(ids.map((id) => getMessageOp(storage, id)));
18599
18772
  const missing = ids.filter((_id, index) => loaded[index] === null);
18600
18773
  if (missing.length) {
18601
18774
  if (attempted < MAX_ATTEMPTS - 1)
@@ -18619,17 +18792,21 @@ function createDurableAutoExtractor(options) {
18619
18792
  );
18620
18793
  continue;
18621
18794
  }
18622
- if (loaded.some(
18623
- (message) => message && (message.decryptionStatus || isEncrypted(message.content))
18624
- )) {
18625
- throw new Error("Source messages are locked; retained for retry");
18795
+ const locked = loaded.filter((message) => message && isEncrypted(message.content));
18796
+ if (locked.length) {
18797
+ throw new LockedSourcesError(
18798
+ locked.flatMap(
18799
+ (message) => message.decryptionStatus === "auth_mismatch" || message.decryptionStatus === "invalid_payload" ? [message.uniqueId] : []
18800
+ )
18801
+ );
18626
18802
  }
18627
18803
  const messages = loaded.flatMap(
18628
18804
  (m) => m && m.conversationId === conversationId && (m.role === "user" || m.role === "assistant") ? [{ id: m.uniqueId, role: m.role, content: m.content }] : []
18629
18805
  );
18630
18806
  let success = messages.length === 0;
18807
+ let result;
18631
18808
  if (messages.length) {
18632
- const result = await extractBatch(
18809
+ result = await extractBatch(
18633
18810
  messages,
18634
18811
  conversationId,
18635
18812
  ids,
@@ -18639,41 +18816,35 @@ function createDurableAutoExtractor(options) {
18639
18816
  success = result.failedCount === 0 && result.outcome !== "empty-after-retry";
18640
18817
  options.onTurnComplete?.(result);
18641
18818
  }
18642
- if (!success) throw new Error("Extraction batch incomplete; retained for retry");
18643
- attempts.delete(job.id);
18644
- const sequences = /* @__PURE__ */ new Map();
18645
- for (const message of loaded)
18646
- if (message && message.messageId > 0)
18647
- sequences.set(message.uniqueId, message.messageId);
18648
- await database.write(async () => {
18649
- const current = idsOf(job);
18650
- const acknowledged = ids.filter((id) => current.includes(id));
18651
- if (!acknowledged.length) return;
18652
- const done = new Set(acknowledged);
18653
- const remaining = current.filter((id) => !done.has(id));
18654
- if (remaining.length) {
18655
- await job.update(
18656
- (r) => r._setRaw("message_ids", JSON.stringify([...acknowledged.slice(-2), ...remaining]))
18657
- );
18658
- } else await job.update((r) => r._setRaw("message_ids", "[]"));
18659
- const newest = acknowledged[acknowledged.length - 1];
18660
- await job.update((r) => {
18661
- r._setRaw("watermark", newest);
18662
- const sequence = sequences.get(newest);
18663
- if (sequence !== void 0) {
18664
- const prior = seqOf(job);
18665
- r._setRaw(
18666
- "watermark_seq",
18667
- prior === void 0 ? sequence : Math.max(prior, sequence)
18819
+ if (!success) {
18820
+ const failure = result?.failure;
18821
+ if (failure?.reason === "http-terminal") {
18822
+ const status = failure.httpStatus;
18823
+ if (status !== void 0 && REQUEST_REJECTED_STATUSES.has(status))
18824
+ throw new BatchFailureError(
18825
+ `Extraction rejected the batch (HTTP ${status}); not retried this session`,
18826
+ true
18668
18827
  );
18669
- }
18670
- });
18671
- });
18828
+ throw new AccountFailureError(
18829
+ `Extraction refused for the account (HTTP ${status ?? "unknown"}); retried next session`
18830
+ );
18831
+ }
18832
+ if ((result?.failedCount ?? 0) > 0 || failure !== void 0 && CONTENT_FAILURE_REASONS.has(failure.reason))
18833
+ throw new BatchFailureError("Extraction batch incomplete; retained for retry", false);
18834
+ throw new Error("Extraction batch incomplete; retained for retry");
18835
+ }
18836
+ await acknowledge(job, ids, loaded, false);
18672
18837
  scheduleAfterSuccess = true;
18673
18838
  } catch (error) {
18674
18839
  attempts.set(job.id, attempted + 1);
18675
18840
  retry = true;
18676
18841
  reportError(error, conversationId);
18842
+ try {
18843
+ if (await recordFailure(job, ids, loaded, error, conversationId))
18844
+ scheduleAfterSuccess = true;
18845
+ } catch (recordError) {
18846
+ reportError(recordError, conversationId);
18847
+ }
18677
18848
  }
18678
18849
  }
18679
18850
  } catch (error) {
@@ -18743,7 +18914,7 @@ function createDurableAutoExtractor(options) {
18743
18914
  const start = boundary >= 0 ? boundary + 1 : Math.max(0, ids.length - Math.max(1, options.windowSize ?? 6));
18744
18915
  if (start >= ids.length) return;
18745
18916
  const ownWatermark = job?._getRaw("watermark");
18746
- const atOwnWatermark = !!job && boundary >= 0 && (ids[boundary] === ownWatermark || sequences.get(ids[boundary]) !== void 0 && sequences.get(ids[boundary]) === seqOf(job));
18917
+ 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));
18747
18918
  const overlapStart = atOwnWatermark ? Math.max(0, boundary - 1) : start;
18748
18919
  const fresh = ids.slice(overlapStart).filter((id) => !foreignSources.has(id));
18749
18920
  if (!fresh.length) return;
@@ -19190,6 +19361,9 @@ var MANUAL_FACT_TYPES = [
19190
19361
  function normalizeManualFactType(value) {
19191
19362
  return typeof value === "string" && MANUAL_FACT_TYPES.includes(value) ? value : void 0;
19192
19363
  }
19364
+ function isSharedScope(scope) {
19365
+ return scope === "shared";
19366
+ }
19193
19367
  async function notifyWritten(options, input, outcome) {
19194
19368
  try {
19195
19369
  await options?.onWritten?.({ input, outcome });
@@ -19258,14 +19432,17 @@ function createMemoryVaultTool(vaultCtx, options, embeddingOptions, cache) {
19258
19432
  try {
19259
19433
  const isUpdate = !!id;
19260
19434
  let previousContent;
19435
+ const scope = options?.scope ?? "private";
19261
19436
  if (isUpdate) {
19262
19437
  const existing = await getVaultMemoryOp(vaultCtx, id);
19263
19438
  if (!existing) {
19264
19439
  return `Error: Memory with ID "${id}" not found. Creating a new memory instead would require a separate call without an ID.`;
19265
19440
  }
19441
+ if (isSharedScope(existing.scope) !== isSharedScope(scope)) {
19442
+ 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.`;
19443
+ }
19266
19444
  previousContent = existing.content;
19267
19445
  }
19268
- const scope = options?.scope ?? "private";
19269
19446
  const operation = {
19270
19447
  action: isUpdate ? "update" : "add",
19271
19448
  content,
@@ -23504,7 +23681,7 @@ __decorateClass([
23504
23681
  ], VaultFolder.prototype, "context", 2);
23505
23682
 
23506
23683
  // src/lib/db/schema.ts
23507
- var SDK_SCHEMA_VERSION = 46;
23684
+ var SDK_SCHEMA_VERSION = 47;
23508
23685
  var sdkSchema = appSchema2({
23509
23686
  version: SDK_SCHEMA_VERSION,
23510
23687
  tables: [
@@ -23520,7 +23697,11 @@ var sdkSchema = appSchema2({
23520
23697
  // source. `watermark` alone is an id, and deleting that message erased
23521
23698
  // the anchor — see the column note in extractionJobs/models.ts.
23522
23699
  { name: "watermark_seq", type: "number", isOptional: true },
23523
- { name: "folder_id", type: "string", isOptional: true }
23700
+ { name: "folder_id", type: "string", isOptional: true },
23701
+ // Poison accounting for the head batch; see extractionJobs/models.ts.
23702
+ { name: "failed_sessions", type: "number", isOptional: true },
23703
+ { name: "failed_head", type: "string", isOptional: true },
23704
+ { name: "failed_at", type: "number", isOptional: true }
23524
23705
  ]
23525
23706
  }),
23526
23707
  // Chat storage tables
@@ -24544,6 +24725,22 @@ var sdkMigrations = schemaMigrations2({
24544
24725
  ]
24545
24726
  })
24546
24727
  ]
24728
+ },
24729
+ // v46 -> v47: persisted poison count (and when it was last counted) for a
24730
+ // durable-extraction job's head batch. NULL = never failed, which is right
24731
+ // for every existing row.
24732
+ {
24733
+ toVersion: 47,
24734
+ steps: [
24735
+ addColumns2({
24736
+ table: "memory_extraction_jobs",
24737
+ columns: [
24738
+ { name: "failed_sessions", type: "number", isOptional: true },
24739
+ { name: "failed_head", type: "string", isOptional: true },
24740
+ { name: "failed_at", type: "number", isOptional: true }
24741
+ ]
24742
+ })
24743
+ ]
24547
24744
  }
24548
24745
  ]
24549
24746
  });