@anuma/sdk 6.25.1 → 6.25.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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(
@@ -18178,7 +18202,10 @@ function reviewedMemoryIdsSignature(ids) {
18178
18202
  return [...new Set(ids)].sort().join("\n");
18179
18203
  }
18180
18204
  function facetsSignature(facets) {
18181
- return facets.map((f) => JSON.stringify([f.key, f.label, f.query, f.guidance, facetResponseSchema(f.key)])).sort().join("\n");
18205
+ return [
18206
+ JSON.stringify(FACET_SYSTEM_PROMPT),
18207
+ ...facets.map((f) => JSON.stringify([f.key, f.label, f.query, f.guidance, facetResponseSchema(f.key)])).sort()
18208
+ ].join("\n");
18182
18209
  }
18183
18210
  function trendsEqual(a, b) {
18184
18211
  if (!a) return false;
@@ -18400,7 +18427,7 @@ var FACET_SYSTEM_PROMPT = `You are writing one section of a person's shareable p
18400
18427
  Rules:
18401
18428
  - Ground every claim in the supplied memories \u2014 never invent, infer beyond, or embellish what they support.
18402
18429
  - If the memories don't cover this section, return an empty summary with hasEvidence=false. Do not pad or guess.
18403
- - Write in third person about the person, in a natural voice suitable for a public profile (no "I"/"you", no name repetition).
18430
+ - Write in a pronoun-free profile voice: lead with the descriptor or the verb ("Thoughtful, detail-oriented builder. Values clear communication."). Never use "they"/"he"/"she", "I", "you", or the person's name \u2014 the person publishes this as their own profile, and strangers read it there.
18404
18431
  - Be concise and specific; no preamble, hedging, or meta-commentary.
18405
18432
  - Respond as JSON in exactly the shape the user turn states, with no extra fields.`;
18406
18433
  function buildFacetUserInstructions(facet) {
@@ -19075,6 +19102,37 @@ async function assembleMemoryContext(options) {
19075
19102
  var import_watermelondb12 = require("@nozbe/watermelondb");
19076
19103
  var MAX_JOBS_PER_PASS = 3;
19077
19104
  var MAX_ATTEMPTS = 3;
19105
+ var MAX_FAILED_SESSIONS = 3;
19106
+ var MIN_FAILED_SESSION_GAP_MS = 60 * 60 * 1e3;
19107
+ var REQUEST_REJECTED_STATUSES = /* @__PURE__ */ new Set([400, 404, 413, 422]);
19108
+ var CONTENT_FAILURE_REASONS = /* @__PURE__ */ new Set([
19109
+ "empty-content",
19110
+ "invalid-json",
19111
+ "null-completion",
19112
+ "body-parse-failed"
19113
+ ]);
19114
+ var BACKOFF_ALLOWANCE_MS = 2100;
19115
+ var RETAIN_BUDGET_MS = 12e4;
19116
+ function defaultBatchTimeoutMs(extract) {
19117
+ const attempts = Math.max(1, extract.maxAttempts ?? 3);
19118
+ const calls = (extract.timeoutMs ?? 6e4) * attempts + BACKOFF_ALLOWANCE_MS * (attempts - 1);
19119
+ const extraction = extract.totalTimeoutMs !== void 0 ? Math.min(extract.totalTimeoutMs, calls) : calls;
19120
+ return extraction + RETAIN_BUDGET_MS;
19121
+ }
19122
+ var BatchFailureError = class extends Error {
19123
+ constructor(message, terminal) {
19124
+ super(message);
19125
+ this.terminal = terminal;
19126
+ }
19127
+ };
19128
+ var AccountFailureError = class extends Error {
19129
+ };
19130
+ var LockedSourcesError = class extends Error {
19131
+ constructor(persistentIds) {
19132
+ super("Source messages are locked; retained for retry");
19133
+ this.persistentIds = persistentIds;
19134
+ }
19135
+ };
19078
19136
  function idsOf(job) {
19079
19137
  return JSON.parse(String(job._getRaw("message_ids")));
19080
19138
  }
@@ -19082,6 +19140,16 @@ function seqOf(job) {
19082
19140
  const raw = job._getRaw("watermark_seq");
19083
19141
  return typeof raw === "number" && raw > 0 ? raw : void 0;
19084
19142
  }
19143
+ function failedSessionsOf(job, head) {
19144
+ if (job._getRaw("failed_head") !== head) return 0;
19145
+ const raw = job._getRaw("failed_sessions");
19146
+ return typeof raw === "number" && raw > 0 ? raw : 0;
19147
+ }
19148
+ function failedAtOf(job, head) {
19149
+ if (job._getRaw("failed_head") !== head) return void 0;
19150
+ const raw = job._getRaw("failed_at");
19151
+ return typeof raw === "number" ? raw : void 0;
19152
+ }
19085
19153
  function createDurableAutoExtractor(options) {
19086
19154
  const vault = options.retainCtx.vaultCtx;
19087
19155
  const database = vault.database;
@@ -19104,6 +19172,9 @@ function createDurableAutoExtractor(options) {
19104
19172
  let writes = Promise.resolve();
19105
19173
  let storeFailures = 0;
19106
19174
  const attempts = /* @__PURE__ */ new Map();
19175
+ const sessionFailures = /* @__PURE__ */ new Map();
19176
+ const skippedThisSession = /* @__PURE__ */ new Set();
19177
+ const clock = options.now ?? Date.now;
19107
19178
  const reportError = (error, conversationId) => {
19108
19179
  try {
19109
19180
  options.onError?.(error instanceof Error ? error : new Error(String(error)), conversationId);
@@ -19132,6 +19203,7 @@ function createDurableAutoExtractor(options) {
19132
19203
  }
19133
19204
  async function extractBatch(messages, conversationId, ids, jobScope, jobFolderId) {
19134
19205
  let watchdog;
19206
+ let cancelled = false;
19135
19207
  const model = options.modelForScope?.(jobScope) ?? options.extract.model;
19136
19208
  try {
19137
19209
  return await Promise.race([
@@ -19142,7 +19214,7 @@ function createDurableAutoExtractor(options) {
19142
19214
  ...options.retainCtx,
19143
19215
  vaultCtx: {
19144
19216
  ...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
19217
+ 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
19218
  }
19147
19219
  },
19148
19220
  // The model follows the job's scope, not the mode alive at drain time.
@@ -19162,14 +19234,116 @@ function createDurableAutoExtractor(options) {
19162
19234
  new Promise((_resolve, reject) => {
19163
19235
  watchdog = setTimeout(
19164
19236
  () => reject(new Error("Extraction did not settle in time; retained for retry")),
19165
- options.batchTimeoutMs ?? 18e4
19237
+ options.batchTimeoutMs ?? defaultBatchTimeoutMs(options.extract)
19166
19238
  );
19167
19239
  })
19168
19240
  ]);
19169
19241
  } finally {
19242
+ cancelled = true;
19170
19243
  if (watchdog !== void 0) clearTimeout(watchdog);
19171
19244
  }
19172
19245
  }
19246
+ async function acknowledge(job, ids, loaded, abandoned) {
19247
+ attempts.delete(job.id);
19248
+ sessionFailures.delete(job.id);
19249
+ skippedThisSession.delete(job.id);
19250
+ const sequences = /* @__PURE__ */ new Map();
19251
+ for (const message of loaded)
19252
+ if (message && message.messageId > 0) sequences.set(message.uniqueId, message.messageId);
19253
+ await database.write(async () => {
19254
+ const current = idsOf(job);
19255
+ const acknowledged = ids.filter((id) => current.includes(id));
19256
+ if (!acknowledged.length) return;
19257
+ const done = new Set(acknowledged);
19258
+ const remaining = current.filter((id) => !done.has(id));
19259
+ await job.update((r) => {
19260
+ r._setRaw(
19261
+ "message_ids",
19262
+ JSON.stringify(
19263
+ remaining.length ? [...abandoned ? [] : acknowledged.slice(-2), ...remaining] : []
19264
+ )
19265
+ );
19266
+ const newest = acknowledged[acknowledged.length - 1];
19267
+ r._setRaw("watermark", newest);
19268
+ const sequence = sequences.get(newest);
19269
+ if (sequence !== void 0) {
19270
+ const prior = seqOf(job);
19271
+ r._setRaw("watermark_seq", prior === void 0 ? sequence : Math.max(prior, sequence));
19272
+ }
19273
+ r._setRaw("failed_sessions", abandoned ? MAX_FAILED_SESSIONS : null);
19274
+ r._setRaw("failed_head", abandoned ? newest : null);
19275
+ r._setRaw("failed_at", null);
19276
+ });
19277
+ });
19278
+ }
19279
+ async function recordFailure(job, ids, loaded, error, conversationId) {
19280
+ const head = ids[0];
19281
+ if (!head || !loaded.some(Boolean)) return false;
19282
+ if (error instanceof AccountFailureError) {
19283
+ skippedThisSession.add(job.id);
19284
+ return false;
19285
+ }
19286
+ const counted = error instanceof BatchFailureError || error instanceof LockedSourcesError && error.persistentIds.length > 0;
19287
+ if (!counted) return false;
19288
+ const prior = sessionFailures.get(job.id);
19289
+ const count = prior?.head === head ? prior.count + 1 : 1;
19290
+ sessionFailures.set(job.id, { head, count });
19291
+ const terminal = error instanceof BatchFailureError && error.terminal;
19292
+ if (!terminal && count < MAX_ATTEMPTS) return false;
19293
+ skippedThisSession.add(job.id);
19294
+ const now = clock();
19295
+ const lastCounted = failedAtOf(job, head);
19296
+ if (lastCounted !== void 0 && now - lastCounted < MIN_FAILED_SESSION_GAP_MS) return false;
19297
+ const failures = failedSessionsOf(job, head) + 1;
19298
+ if (failures < MAX_FAILED_SESSIONS) {
19299
+ await database.write(
19300
+ () => job.update((r) => {
19301
+ r._setRaw("failed_sessions", failures);
19302
+ r._setRaw("failed_head", head);
19303
+ r._setRaw("failed_at", now);
19304
+ })
19305
+ );
19306
+ return false;
19307
+ }
19308
+ if (error instanceof LockedSourcesError) {
19309
+ const dropped = new Set(error.persistentIds);
19310
+ let newest;
19311
+ for (const message of loaded)
19312
+ if (message && dropped.has(message.uniqueId) && message.messageId > (newest?.seq ?? 0))
19313
+ newest = { id: message.uniqueId, seq: message.messageId };
19314
+ const advance = newest !== void 0 && newest.seq > (seqOf(job) ?? 0) ? newest : void 0;
19315
+ await database.write(
19316
+ () => job.update((r) => {
19317
+ r._setRaw("message_ids", JSON.stringify(idsOf(job).filter((id) => !dropped.has(id))));
19318
+ if (advance) {
19319
+ r._setRaw("watermark", advance.id);
19320
+ r._setRaw("watermark_seq", advance.seq);
19321
+ }
19322
+ r._setRaw("failed_sessions", advance ? MAX_FAILED_SESSIONS : null);
19323
+ r._setRaw("failed_head", advance ? advance.id : null);
19324
+ r._setRaw("failed_at", null);
19325
+ })
19326
+ );
19327
+ attempts.delete(job.id);
19328
+ sessionFailures.delete(job.id);
19329
+ skippedThisSession.delete(job.id);
19330
+ reportError(
19331
+ new Error(
19332
+ `Dropped ${dropped.size} source id(s) that stayed locked for ${failures} sessions to unblock extraction`
19333
+ ),
19334
+ conversationId
19335
+ );
19336
+ return true;
19337
+ }
19338
+ await acknowledge(job, ids, loaded, true);
19339
+ reportError(
19340
+ new Error(
19341
+ `Abandoned an extraction batch of ${ids.length} source id(s) after it failed in ${failures} sessions`
19342
+ ),
19343
+ conversationId
19344
+ );
19345
+ return true;
19346
+ }
19173
19347
  async function drain() {
19174
19348
  if (disposed || running) return;
19175
19349
  running = true;
@@ -19184,21 +19358,23 @@ function createDurableAutoExtractor(options) {
19184
19358
  for (const job of pending) {
19185
19359
  if (disposed) break;
19186
19360
  const attempted = attempts.get(job.id) ?? 0;
19187
- if (attempted >= MAX_ATTEMPTS) continue;
19361
+ if (attempted >= MAX_ATTEMPTS || skippedThisSession.has(job.id)) continue;
19188
19362
  if (processed >= MAX_JOBS_PER_PASS) {
19189
19363
  scheduleAfterSuccess = true;
19190
19364
  break;
19191
19365
  }
19192
19366
  processed++;
19193
19367
  const conversationId = String(job._getRaw("conversation_id"));
19368
+ let ids = [];
19369
+ let loaded = [];
19194
19370
  try {
19195
19371
  const conversations = await storage.conversationsCollection.query(import_watermelondb12.Q.where("conversation_id", conversationId), import_watermelondb12.Q.where("is_deleted", false)).fetchCount();
19196
19372
  if (!conversations) {
19197
19373
  await database.write(() => job.destroyPermanently());
19198
19374
  continue;
19199
19375
  }
19200
- const ids = idsOf(job).slice(0, 20);
19201
- const loaded = await Promise.all(ids.map((id) => getMessageOp(storage, id)));
19376
+ ids = idsOf(job).slice(0, 20);
19377
+ loaded = await Promise.all(ids.map((id) => getMessageOp(storage, id)));
19202
19378
  const missing = ids.filter((_id, index) => loaded[index] === null);
19203
19379
  if (missing.length) {
19204
19380
  if (attempted < MAX_ATTEMPTS - 1)
@@ -19222,17 +19398,21 @@ function createDurableAutoExtractor(options) {
19222
19398
  );
19223
19399
  continue;
19224
19400
  }
19225
- if (loaded.some(
19226
- (message) => message && (message.decryptionStatus || isEncrypted(message.content))
19227
- )) {
19228
- throw new Error("Source messages are locked; retained for retry");
19401
+ const locked = loaded.filter((message) => message && isEncrypted(message.content));
19402
+ if (locked.length) {
19403
+ throw new LockedSourcesError(
19404
+ locked.flatMap(
19405
+ (message) => message.decryptionStatus === "auth_mismatch" || message.decryptionStatus === "invalid_payload" ? [message.uniqueId] : []
19406
+ )
19407
+ );
19229
19408
  }
19230
19409
  const messages = loaded.flatMap(
19231
19410
  (m) => m && m.conversationId === conversationId && (m.role === "user" || m.role === "assistant") ? [{ id: m.uniqueId, role: m.role, content: m.content }] : []
19232
19411
  );
19233
19412
  let success = messages.length === 0;
19413
+ let result;
19234
19414
  if (messages.length) {
19235
- const result = await extractBatch(
19415
+ result = await extractBatch(
19236
19416
  messages,
19237
19417
  conversationId,
19238
19418
  ids,
@@ -19242,41 +19422,35 @@ function createDurableAutoExtractor(options) {
19242
19422
  success = result.failedCount === 0 && result.outcome !== "empty-after-retry";
19243
19423
  options.onTurnComplete?.(result);
19244
19424
  }
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)
19425
+ if (!success) {
19426
+ const failure = result?.failure;
19427
+ if (failure?.reason === "http-terminal") {
19428
+ const status = failure.httpStatus;
19429
+ if (status !== void 0 && REQUEST_REJECTED_STATUSES.has(status))
19430
+ throw new BatchFailureError(
19431
+ `Extraction rejected the batch (HTTP ${status}); not retried this session`,
19432
+ true
19271
19433
  );
19272
- }
19273
- });
19274
- });
19434
+ throw new AccountFailureError(
19435
+ `Extraction refused for the account (HTTP ${status ?? "unknown"}); retried next session`
19436
+ );
19437
+ }
19438
+ if ((result?.failedCount ?? 0) > 0 || failure !== void 0 && CONTENT_FAILURE_REASONS.has(failure.reason))
19439
+ throw new BatchFailureError("Extraction batch incomplete; retained for retry", false);
19440
+ throw new Error("Extraction batch incomplete; retained for retry");
19441
+ }
19442
+ await acknowledge(job, ids, loaded, false);
19275
19443
  scheduleAfterSuccess = true;
19276
19444
  } catch (error) {
19277
19445
  attempts.set(job.id, attempted + 1);
19278
19446
  retry = true;
19279
19447
  reportError(error, conversationId);
19448
+ try {
19449
+ if (await recordFailure(job, ids, loaded, error, conversationId))
19450
+ scheduleAfterSuccess = true;
19451
+ } catch (recordError) {
19452
+ reportError(recordError, conversationId);
19453
+ }
19280
19454
  }
19281
19455
  }
19282
19456
  } catch (error) {
@@ -19346,7 +19520,7 @@ function createDurableAutoExtractor(options) {
19346
19520
  const start = boundary >= 0 ? boundary + 1 : Math.max(0, ids.length - Math.max(1, options.windowSize ?? 6));
19347
19521
  if (start >= ids.length) return;
19348
19522
  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));
19523
+ 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
19524
  const overlapStart = atOwnWatermark ? Math.max(0, boundary - 1) : start;
19351
19525
  const fresh = ids.slice(overlapStart).filter((id) => !foreignSources.has(id));
19352
19526
  if (!fresh.length) return;
@@ -19793,6 +19967,9 @@ var MANUAL_FACT_TYPES = [
19793
19967
  function normalizeManualFactType(value) {
19794
19968
  return typeof value === "string" && MANUAL_FACT_TYPES.includes(value) ? value : void 0;
19795
19969
  }
19970
+ function isSharedScope(scope) {
19971
+ return scope === "shared";
19972
+ }
19796
19973
  async function notifyWritten(options, input, outcome) {
19797
19974
  try {
19798
19975
  await options?.onWritten?.({ input, outcome });
@@ -19861,14 +20038,17 @@ function createMemoryVaultTool(vaultCtx, options, embeddingOptions, cache) {
19861
20038
  try {
19862
20039
  const isUpdate = !!id;
19863
20040
  let previousContent;
20041
+ const scope = options?.scope ?? "private";
19864
20042
  if (isUpdate) {
19865
20043
  const existing = await getVaultMemoryOp(vaultCtx, id);
19866
20044
  if (!existing) {
19867
20045
  return `Error: Memory with ID "${id}" not found. Creating a new memory instead would require a separate call without an ID.`;
19868
20046
  }
20047
+ if (isSharedScope(existing.scope) !== isSharedScope(scope)) {
20048
+ 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.`;
20049
+ }
19869
20050
  previousContent = existing.content;
19870
20051
  }
19871
- const scope = options?.scope ?? "private";
19872
20052
  const operation = {
19873
20053
  action: isUpdate ? "update" : "add",
19874
20054
  content,
@@ -22931,7 +23111,7 @@ __decorateClass([
22931
23111
  ], VaultFolder.prototype, "context", 2);
22932
23112
 
22933
23113
  // src/lib/db/schema.ts
22934
- var SDK_SCHEMA_VERSION = 46;
23114
+ var SDK_SCHEMA_VERSION = 47;
22935
23115
  var sdkSchema = (0, import_watermelondb21.appSchema)({
22936
23116
  version: SDK_SCHEMA_VERSION,
22937
23117
  tables: [
@@ -22947,7 +23127,11 @@ var sdkSchema = (0, import_watermelondb21.appSchema)({
22947
23127
  // source. `watermark` alone is an id, and deleting that message erased
22948
23128
  // the anchor — see the column note in extractionJobs/models.ts.
22949
23129
  { name: "watermark_seq", type: "number", isOptional: true },
22950
- { name: "folder_id", type: "string", isOptional: true }
23130
+ { name: "folder_id", type: "string", isOptional: true },
23131
+ // Poison accounting for the head batch; see extractionJobs/models.ts.
23132
+ { name: "failed_sessions", type: "number", isOptional: true },
23133
+ { name: "failed_head", type: "string", isOptional: true },
23134
+ { name: "failed_at", type: "number", isOptional: true }
22951
23135
  ]
22952
23136
  }),
22953
23137
  // Chat storage tables
@@ -23971,6 +24155,22 @@ var sdkMigrations = (0, import_migrations2.schemaMigrations)({
23971
24155
  ]
23972
24156
  })
23973
24157
  ]
24158
+ },
24159
+ // v46 -> v47: persisted poison count (and when it was last counted) for a
24160
+ // durable-extraction job's head batch. NULL = never failed, which is right
24161
+ // for every existing row.
24162
+ {
24163
+ toVersion: 47,
24164
+ steps: [
24165
+ (0, import_migrations2.addColumns)({
24166
+ table: "memory_extraction_jobs",
24167
+ columns: [
24168
+ { name: "failed_sessions", type: "number", isOptional: true },
24169
+ { name: "failed_head", type: "string", isOptional: true },
24170
+ { name: "failed_at", type: "number", isOptional: true }
24171
+ ]
24172
+ })
24173
+ ]
23974
24174
  }
23975
24175
  ]
23976
24176
  });