@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.
@@ -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(
@@ -17581,7 +17605,10 @@ function reviewedMemoryIdsSignature(ids) {
17581
17605
  return [...new Set(ids)].sort().join("\n");
17582
17606
  }
17583
17607
  function facetsSignature(facets) {
17584
- return facets.map((f) => JSON.stringify([f.key, f.label, f.query, f.guidance, facetResponseSchema(f.key)])).sort().join("\n");
17608
+ return [
17609
+ JSON.stringify(FACET_SYSTEM_PROMPT),
17610
+ ...facets.map((f) => JSON.stringify([f.key, f.label, f.query, f.guidance, facetResponseSchema(f.key)])).sort()
17611
+ ].join("\n");
17585
17612
  }
17586
17613
  function trendsEqual(a, b) {
17587
17614
  if (!a) return false;
@@ -17803,7 +17830,7 @@ var FACET_SYSTEM_PROMPT = `You are writing one section of a person's shareable p
17803
17830
  Rules:
17804
17831
  - Ground every claim in the supplied memories \u2014 never invent, infer beyond, or embellish what they support.
17805
17832
  - If the memories don't cover this section, return an empty summary with hasEvidence=false. Do not pad or guess.
17806
- - Write in third person about the person, in a natural voice suitable for a public profile (no "I"/"you", no name repetition).
17833
+ - 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.
17807
17834
  - Be concise and specific; no preamble, hedging, or meta-commentary.
17808
17835
  - Respond as JSON in exactly the shape the user turn states, with no extra fields.`;
17809
17836
  function buildFacetUserInstructions(facet) {
@@ -18472,6 +18499,37 @@ async function assembleMemoryContext(options) {
18472
18499
  import { Q as Q8 } from "@nozbe/watermelondb";
18473
18500
  var MAX_JOBS_PER_PASS = 3;
18474
18501
  var MAX_ATTEMPTS = 3;
18502
+ var MAX_FAILED_SESSIONS = 3;
18503
+ var MIN_FAILED_SESSION_GAP_MS = 60 * 60 * 1e3;
18504
+ var REQUEST_REJECTED_STATUSES = /* @__PURE__ */ new Set([400, 404, 413, 422]);
18505
+ var CONTENT_FAILURE_REASONS = /* @__PURE__ */ new Set([
18506
+ "empty-content",
18507
+ "invalid-json",
18508
+ "null-completion",
18509
+ "body-parse-failed"
18510
+ ]);
18511
+ var BACKOFF_ALLOWANCE_MS = 2100;
18512
+ var RETAIN_BUDGET_MS = 12e4;
18513
+ function defaultBatchTimeoutMs(extract) {
18514
+ const attempts = Math.max(1, extract.maxAttempts ?? 3);
18515
+ const calls = (extract.timeoutMs ?? 6e4) * attempts + BACKOFF_ALLOWANCE_MS * (attempts - 1);
18516
+ const extraction = extract.totalTimeoutMs !== void 0 ? Math.min(extract.totalTimeoutMs, calls) : calls;
18517
+ return extraction + RETAIN_BUDGET_MS;
18518
+ }
18519
+ var BatchFailureError = class extends Error {
18520
+ constructor(message, terminal) {
18521
+ super(message);
18522
+ this.terminal = terminal;
18523
+ }
18524
+ };
18525
+ var AccountFailureError = class extends Error {
18526
+ };
18527
+ var LockedSourcesError = class extends Error {
18528
+ constructor(persistentIds) {
18529
+ super("Source messages are locked; retained for retry");
18530
+ this.persistentIds = persistentIds;
18531
+ }
18532
+ };
18475
18533
  function idsOf(job) {
18476
18534
  return JSON.parse(String(job._getRaw("message_ids")));
18477
18535
  }
@@ -18479,6 +18537,16 @@ function seqOf(job) {
18479
18537
  const raw = job._getRaw("watermark_seq");
18480
18538
  return typeof raw === "number" && raw > 0 ? raw : void 0;
18481
18539
  }
18540
+ function failedSessionsOf(job, head) {
18541
+ if (job._getRaw("failed_head") !== head) return 0;
18542
+ const raw = job._getRaw("failed_sessions");
18543
+ return typeof raw === "number" && raw > 0 ? raw : 0;
18544
+ }
18545
+ function failedAtOf(job, head) {
18546
+ if (job._getRaw("failed_head") !== head) return void 0;
18547
+ const raw = job._getRaw("failed_at");
18548
+ return typeof raw === "number" ? raw : void 0;
18549
+ }
18482
18550
  function createDurableAutoExtractor(options) {
18483
18551
  const vault = options.retainCtx.vaultCtx;
18484
18552
  const database = vault.database;
@@ -18501,6 +18569,9 @@ function createDurableAutoExtractor(options) {
18501
18569
  let writes = Promise.resolve();
18502
18570
  let storeFailures = 0;
18503
18571
  const attempts = /* @__PURE__ */ new Map();
18572
+ const sessionFailures = /* @__PURE__ */ new Map();
18573
+ const skippedThisSession = /* @__PURE__ */ new Set();
18574
+ const clock = options.now ?? Date.now;
18504
18575
  const reportError = (error, conversationId) => {
18505
18576
  try {
18506
18577
  options.onError?.(error instanceof Error ? error : new Error(String(error)), conversationId);
@@ -18529,6 +18600,7 @@ function createDurableAutoExtractor(options) {
18529
18600
  }
18530
18601
  async function extractBatch(messages, conversationId, ids, jobScope, jobFolderId) {
18531
18602
  let watchdog;
18603
+ let cancelled = false;
18532
18604
  const model = options.modelForScope?.(jobScope) ?? options.extract.model;
18533
18605
  try {
18534
18606
  return await Promise.race([
@@ -18539,7 +18611,7 @@ function createDurableAutoExtractor(options) {
18539
18611
  ...options.retainCtx,
18540
18612
  vaultCtx: {
18541
18613
  ...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
18614
+ 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
18615
  }
18544
18616
  },
18545
18617
  // The model follows the job's scope, not the mode alive at drain time.
@@ -18559,14 +18631,116 @@ function createDurableAutoExtractor(options) {
18559
18631
  new Promise((_resolve, reject) => {
18560
18632
  watchdog = setTimeout(
18561
18633
  () => reject(new Error("Extraction did not settle in time; retained for retry")),
18562
- options.batchTimeoutMs ?? 18e4
18634
+ options.batchTimeoutMs ?? defaultBatchTimeoutMs(options.extract)
18563
18635
  );
18564
18636
  })
18565
18637
  ]);
18566
18638
  } finally {
18639
+ cancelled = true;
18567
18640
  if (watchdog !== void 0) clearTimeout(watchdog);
18568
18641
  }
18569
18642
  }
18643
+ async function acknowledge(job, ids, loaded, abandoned) {
18644
+ attempts.delete(job.id);
18645
+ sessionFailures.delete(job.id);
18646
+ skippedThisSession.delete(job.id);
18647
+ const sequences = /* @__PURE__ */ new Map();
18648
+ for (const message of loaded)
18649
+ if (message && message.messageId > 0) sequences.set(message.uniqueId, message.messageId);
18650
+ await database.write(async () => {
18651
+ const current = idsOf(job);
18652
+ const acknowledged = ids.filter((id) => current.includes(id));
18653
+ if (!acknowledged.length) return;
18654
+ const done = new Set(acknowledged);
18655
+ const remaining = current.filter((id) => !done.has(id));
18656
+ await job.update((r) => {
18657
+ r._setRaw(
18658
+ "message_ids",
18659
+ JSON.stringify(
18660
+ remaining.length ? [...abandoned ? [] : acknowledged.slice(-2), ...remaining] : []
18661
+ )
18662
+ );
18663
+ const newest = acknowledged[acknowledged.length - 1];
18664
+ r._setRaw("watermark", newest);
18665
+ const sequence = sequences.get(newest);
18666
+ if (sequence !== void 0) {
18667
+ const prior = seqOf(job);
18668
+ r._setRaw("watermark_seq", prior === void 0 ? sequence : Math.max(prior, sequence));
18669
+ }
18670
+ r._setRaw("failed_sessions", abandoned ? MAX_FAILED_SESSIONS : null);
18671
+ r._setRaw("failed_head", abandoned ? newest : null);
18672
+ r._setRaw("failed_at", null);
18673
+ });
18674
+ });
18675
+ }
18676
+ async function recordFailure(job, ids, loaded, error, conversationId) {
18677
+ const head = ids[0];
18678
+ if (!head || !loaded.some(Boolean)) return false;
18679
+ if (error instanceof AccountFailureError) {
18680
+ skippedThisSession.add(job.id);
18681
+ return false;
18682
+ }
18683
+ const counted = error instanceof BatchFailureError || error instanceof LockedSourcesError && error.persistentIds.length > 0;
18684
+ if (!counted) return false;
18685
+ const prior = sessionFailures.get(job.id);
18686
+ const count = prior?.head === head ? prior.count + 1 : 1;
18687
+ sessionFailures.set(job.id, { head, count });
18688
+ const terminal = error instanceof BatchFailureError && error.terminal;
18689
+ if (!terminal && count < MAX_ATTEMPTS) return false;
18690
+ skippedThisSession.add(job.id);
18691
+ const now = clock();
18692
+ const lastCounted = failedAtOf(job, head);
18693
+ if (lastCounted !== void 0 && now - lastCounted < MIN_FAILED_SESSION_GAP_MS) return false;
18694
+ const failures = failedSessionsOf(job, head) + 1;
18695
+ if (failures < MAX_FAILED_SESSIONS) {
18696
+ await database.write(
18697
+ () => job.update((r) => {
18698
+ r._setRaw("failed_sessions", failures);
18699
+ r._setRaw("failed_head", head);
18700
+ r._setRaw("failed_at", now);
18701
+ })
18702
+ );
18703
+ return false;
18704
+ }
18705
+ if (error instanceof LockedSourcesError) {
18706
+ const dropped = new Set(error.persistentIds);
18707
+ let newest;
18708
+ for (const message of loaded)
18709
+ if (message && dropped.has(message.uniqueId) && message.messageId > (newest?.seq ?? 0))
18710
+ newest = { id: message.uniqueId, seq: message.messageId };
18711
+ const advance = newest !== void 0 && newest.seq > (seqOf(job) ?? 0) ? newest : void 0;
18712
+ await database.write(
18713
+ () => job.update((r) => {
18714
+ r._setRaw("message_ids", JSON.stringify(idsOf(job).filter((id) => !dropped.has(id))));
18715
+ if (advance) {
18716
+ r._setRaw("watermark", advance.id);
18717
+ r._setRaw("watermark_seq", advance.seq);
18718
+ }
18719
+ r._setRaw("failed_sessions", advance ? MAX_FAILED_SESSIONS : null);
18720
+ r._setRaw("failed_head", advance ? advance.id : null);
18721
+ r._setRaw("failed_at", null);
18722
+ })
18723
+ );
18724
+ attempts.delete(job.id);
18725
+ sessionFailures.delete(job.id);
18726
+ skippedThisSession.delete(job.id);
18727
+ reportError(
18728
+ new Error(
18729
+ `Dropped ${dropped.size} source id(s) that stayed locked for ${failures} sessions to unblock extraction`
18730
+ ),
18731
+ conversationId
18732
+ );
18733
+ return true;
18734
+ }
18735
+ await acknowledge(job, ids, loaded, true);
18736
+ reportError(
18737
+ new Error(
18738
+ `Abandoned an extraction batch of ${ids.length} source id(s) after it failed in ${failures} sessions`
18739
+ ),
18740
+ conversationId
18741
+ );
18742
+ return true;
18743
+ }
18570
18744
  async function drain() {
18571
18745
  if (disposed || running) return;
18572
18746
  running = true;
@@ -18581,21 +18755,23 @@ function createDurableAutoExtractor(options) {
18581
18755
  for (const job of pending) {
18582
18756
  if (disposed) break;
18583
18757
  const attempted = attempts.get(job.id) ?? 0;
18584
- if (attempted >= MAX_ATTEMPTS) continue;
18758
+ if (attempted >= MAX_ATTEMPTS || skippedThisSession.has(job.id)) continue;
18585
18759
  if (processed >= MAX_JOBS_PER_PASS) {
18586
18760
  scheduleAfterSuccess = true;
18587
18761
  break;
18588
18762
  }
18589
18763
  processed++;
18590
18764
  const conversationId = String(job._getRaw("conversation_id"));
18765
+ let ids = [];
18766
+ let loaded = [];
18591
18767
  try {
18592
18768
  const conversations = await storage.conversationsCollection.query(Q8.where("conversation_id", conversationId), Q8.where("is_deleted", false)).fetchCount();
18593
18769
  if (!conversations) {
18594
18770
  await database.write(() => job.destroyPermanently());
18595
18771
  continue;
18596
18772
  }
18597
- const ids = idsOf(job).slice(0, 20);
18598
- const loaded = await Promise.all(ids.map((id) => getMessageOp(storage, id)));
18773
+ ids = idsOf(job).slice(0, 20);
18774
+ loaded = await Promise.all(ids.map((id) => getMessageOp(storage, id)));
18599
18775
  const missing = ids.filter((_id, index) => loaded[index] === null);
18600
18776
  if (missing.length) {
18601
18777
  if (attempted < MAX_ATTEMPTS - 1)
@@ -18619,17 +18795,21 @@ function createDurableAutoExtractor(options) {
18619
18795
  );
18620
18796
  continue;
18621
18797
  }
18622
- if (loaded.some(
18623
- (message) => message && (message.decryptionStatus || isEncrypted(message.content))
18624
- )) {
18625
- throw new Error("Source messages are locked; retained for retry");
18798
+ const locked = loaded.filter((message) => message && isEncrypted(message.content));
18799
+ if (locked.length) {
18800
+ throw new LockedSourcesError(
18801
+ locked.flatMap(
18802
+ (message) => message.decryptionStatus === "auth_mismatch" || message.decryptionStatus === "invalid_payload" ? [message.uniqueId] : []
18803
+ )
18804
+ );
18626
18805
  }
18627
18806
  const messages = loaded.flatMap(
18628
18807
  (m) => m && m.conversationId === conversationId && (m.role === "user" || m.role === "assistant") ? [{ id: m.uniqueId, role: m.role, content: m.content }] : []
18629
18808
  );
18630
18809
  let success = messages.length === 0;
18810
+ let result;
18631
18811
  if (messages.length) {
18632
- const result = await extractBatch(
18812
+ result = await extractBatch(
18633
18813
  messages,
18634
18814
  conversationId,
18635
18815
  ids,
@@ -18639,41 +18819,35 @@ function createDurableAutoExtractor(options) {
18639
18819
  success = result.failedCount === 0 && result.outcome !== "empty-after-retry";
18640
18820
  options.onTurnComplete?.(result);
18641
18821
  }
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)
18822
+ if (!success) {
18823
+ const failure = result?.failure;
18824
+ if (failure?.reason === "http-terminal") {
18825
+ const status = failure.httpStatus;
18826
+ if (status !== void 0 && REQUEST_REJECTED_STATUSES.has(status))
18827
+ throw new BatchFailureError(
18828
+ `Extraction rejected the batch (HTTP ${status}); not retried this session`,
18829
+ true
18668
18830
  );
18669
- }
18670
- });
18671
- });
18831
+ throw new AccountFailureError(
18832
+ `Extraction refused for the account (HTTP ${status ?? "unknown"}); retried next session`
18833
+ );
18834
+ }
18835
+ if ((result?.failedCount ?? 0) > 0 || failure !== void 0 && CONTENT_FAILURE_REASONS.has(failure.reason))
18836
+ throw new BatchFailureError("Extraction batch incomplete; retained for retry", false);
18837
+ throw new Error("Extraction batch incomplete; retained for retry");
18838
+ }
18839
+ await acknowledge(job, ids, loaded, false);
18672
18840
  scheduleAfterSuccess = true;
18673
18841
  } catch (error) {
18674
18842
  attempts.set(job.id, attempted + 1);
18675
18843
  retry = true;
18676
18844
  reportError(error, conversationId);
18845
+ try {
18846
+ if (await recordFailure(job, ids, loaded, error, conversationId))
18847
+ scheduleAfterSuccess = true;
18848
+ } catch (recordError) {
18849
+ reportError(recordError, conversationId);
18850
+ }
18677
18851
  }
18678
18852
  }
18679
18853
  } catch (error) {
@@ -18743,7 +18917,7 @@ function createDurableAutoExtractor(options) {
18743
18917
  const start = boundary >= 0 ? boundary + 1 : Math.max(0, ids.length - Math.max(1, options.windowSize ?? 6));
18744
18918
  if (start >= ids.length) return;
18745
18919
  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));
18920
+ 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
18921
  const overlapStart = atOwnWatermark ? Math.max(0, boundary - 1) : start;
18748
18922
  const fresh = ids.slice(overlapStart).filter((id) => !foreignSources.has(id));
18749
18923
  if (!fresh.length) return;
@@ -19190,6 +19364,9 @@ var MANUAL_FACT_TYPES = [
19190
19364
  function normalizeManualFactType(value) {
19191
19365
  return typeof value === "string" && MANUAL_FACT_TYPES.includes(value) ? value : void 0;
19192
19366
  }
19367
+ function isSharedScope(scope) {
19368
+ return scope === "shared";
19369
+ }
19193
19370
  async function notifyWritten(options, input, outcome) {
19194
19371
  try {
19195
19372
  await options?.onWritten?.({ input, outcome });
@@ -19258,14 +19435,17 @@ function createMemoryVaultTool(vaultCtx, options, embeddingOptions, cache) {
19258
19435
  try {
19259
19436
  const isUpdate = !!id;
19260
19437
  let previousContent;
19438
+ const scope = options?.scope ?? "private";
19261
19439
  if (isUpdate) {
19262
19440
  const existing = await getVaultMemoryOp(vaultCtx, id);
19263
19441
  if (!existing) {
19264
19442
  return `Error: Memory with ID "${id}" not found. Creating a new memory instead would require a separate call without an ID.`;
19265
19443
  }
19444
+ if (isSharedScope(existing.scope) !== isSharedScope(scope)) {
19445
+ 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.`;
19446
+ }
19266
19447
  previousContent = existing.content;
19267
19448
  }
19268
- const scope = options?.scope ?? "private";
19269
19449
  const operation = {
19270
19450
  action: isUpdate ? "update" : "add",
19271
19451
  content,
@@ -23504,7 +23684,7 @@ __decorateClass([
23504
23684
  ], VaultFolder.prototype, "context", 2);
23505
23685
 
23506
23686
  // src/lib/db/schema.ts
23507
- var SDK_SCHEMA_VERSION = 46;
23687
+ var SDK_SCHEMA_VERSION = 47;
23508
23688
  var sdkSchema = appSchema2({
23509
23689
  version: SDK_SCHEMA_VERSION,
23510
23690
  tables: [
@@ -23520,7 +23700,11 @@ var sdkSchema = appSchema2({
23520
23700
  // source. `watermark` alone is an id, and deleting that message erased
23521
23701
  // the anchor — see the column note in extractionJobs/models.ts.
23522
23702
  { name: "watermark_seq", type: "number", isOptional: true },
23523
- { name: "folder_id", type: "string", isOptional: true }
23703
+ { name: "folder_id", type: "string", isOptional: true },
23704
+ // Poison accounting for the head batch; see extractionJobs/models.ts.
23705
+ { name: "failed_sessions", type: "number", isOptional: true },
23706
+ { name: "failed_head", type: "string", isOptional: true },
23707
+ { name: "failed_at", type: "number", isOptional: true }
23524
23708
  ]
23525
23709
  }),
23526
23710
  // Chat storage tables
@@ -24544,6 +24728,22 @@ var sdkMigrations = schemaMigrations2({
24544
24728
  ]
24545
24729
  })
24546
24730
  ]
24731
+ },
24732
+ // v46 -> v47: persisted poison count (and when it was last counted) for a
24733
+ // durable-extraction job's head batch. NULL = never failed, which is right
24734
+ // for every existing row.
24735
+ {
24736
+ toVersion: 47,
24737
+ steps: [
24738
+ addColumns2({
24739
+ table: "memory_extraction_jobs",
24740
+ columns: [
24741
+ { name: "failed_sessions", type: "number", isOptional: true },
24742
+ { name: "failed_head", type: "string", isOptional: true },
24743
+ { name: "failed_at", type: "number", isOptional: true }
24744
+ ]
24745
+ })
24746
+ ]
24547
24747
  }
24548
24748
  ]
24549
24749
  });