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