@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.
@@ -15457,7 +15457,7 @@ __decorateClass([
15457
15457
  ], VaultFolder.prototype, "context", 2);
15458
15458
 
15459
15459
  // src/lib/db/schema.ts
15460
- var SDK_SCHEMA_VERSION = 46;
15460
+ var SDK_SCHEMA_VERSION = 47;
15461
15461
  var sdkSchema = (0, import_watermelondb13.appSchema)({
15462
15462
  version: SDK_SCHEMA_VERSION,
15463
15463
  tables: [
@@ -15473,7 +15473,11 @@ var sdkSchema = (0, import_watermelondb13.appSchema)({
15473
15473
  // source. `watermark` alone is an id, and deleting that message erased
15474
15474
  // the anchor — see the column note in extractionJobs/models.ts.
15475
15475
  { name: "watermark_seq", type: "number", isOptional: true },
15476
- { name: "folder_id", type: "string", isOptional: true }
15476
+ { name: "folder_id", type: "string", isOptional: true },
15477
+ // Poison accounting for the head batch; see extractionJobs/models.ts.
15478
+ { name: "failed_sessions", type: "number", isOptional: true },
15479
+ { name: "failed_head", type: "string", isOptional: true },
15480
+ { name: "failed_at", type: "number", isOptional: true }
15477
15481
  ]
15478
15482
  }),
15479
15483
  // Chat storage tables
@@ -16497,6 +16501,22 @@ var sdkMigrations = (0, import_migrations.schemaMigrations)({
16497
16501
  ]
16498
16502
  })
16499
16503
  ]
16504
+ },
16505
+ // v46 -> v47: persisted poison count (and when it was last counted) for a
16506
+ // durable-extraction job's head batch. NULL = never failed, which is right
16507
+ // for every existing row.
16508
+ {
16509
+ toVersion: 47,
16510
+ steps: [
16511
+ (0, import_migrations.addColumns)({
16512
+ table: "memory_extraction_jobs",
16513
+ columns: [
16514
+ { name: "failed_sessions", type: "number", isOptional: true },
16515
+ { name: "failed_head", type: "string", isOptional: true },
16516
+ { name: "failed_at", type: "number", isOptional: true }
16517
+ ]
16518
+ })
16519
+ ]
16500
16520
  }
16501
16521
  ]
16502
16522
  });
@@ -20382,8 +20402,42 @@ async function vaultMemoryToStored(memory, walletAddress, signMessage, embeddedW
20382
20402
  }
20383
20403
  return raw;
20384
20404
  }
20405
+ function populateNewVaultMemory(record, ctx, opts, encryptedContent) {
20406
+ record._setRaw("content", encryptedContent);
20407
+ record._setRaw("scope", opts.scope ?? "private");
20408
+ record._setRaw("folder_id", opts.folderId ?? null);
20409
+ record._setRaw("user_id", ctx.userId ?? null);
20410
+ record._setRaw("is_deleted", false);
20411
+ if (opts.embedding !== void 0) {
20412
+ record._setRaw("embedding", opts.embedding);
20413
+ record._setRaw("embedding_model", opts.embeddingModel ?? null);
20414
+ }
20415
+ if (opts.sourceChunkIds !== void 0) {
20416
+ record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
20417
+ }
20418
+ record._setRaw("proof_count", opts.proofCount ?? 1);
20419
+ record._setRaw("source", opts.source ?? "manual");
20420
+ if (opts.eventTime) {
20421
+ record._setRaw("event_time_start", opts.eventTime.start ?? null);
20422
+ record._setRaw("event_time_end", opts.eventTime.end ?? null);
20423
+ record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
20424
+ }
20425
+ if (opts.factType !== void 0) {
20426
+ record._setRaw("fact_type", opts.factType);
20427
+ }
20428
+ if (opts.trustTier !== void 0) {
20429
+ record._setRaw("trust_tier", normalizeTrustTier(opts.trustTier));
20430
+ }
20431
+ record._setRaw("visibility", opts.visibility ?? "private");
20432
+ record._setRaw(
20433
+ "published_at",
20434
+ opts.visibility && opts.visibility !== "private" ? opts.publishedAt ?? Date.now() : null
20435
+ );
20436
+ if (opts.geohash !== void 0) {
20437
+ record._setRaw("geohash", opts.geohash);
20438
+ }
20439
+ }
20385
20440
  async function createVaultMemoryOp(ctx, opts) {
20386
- const scope = opts.scope ?? "private";
20387
20441
  const encryptedContent = ctx.walletAddress && ctx.signMessage ? await encryptVaultMemoryContent(
20388
20442
  opts.content,
20389
20443
  ctx.walletAddress,
@@ -20393,47 +20447,14 @@ async function createVaultMemoryOp(ctx, opts) {
20393
20447
  const created = await ctx.database.write(async () => {
20394
20448
  if (ctx.canWrite && !await ctx.canWrite())
20395
20449
  throw new Error("Memory source is no longer eligible");
20396
- return ctx.vaultMemoryCollection.create((record) => {
20397
- record._setRaw("content", encryptedContent);
20398
- record._setRaw("scope", scope);
20399
- record._setRaw("folder_id", opts.folderId ?? null);
20400
- record._setRaw("user_id", ctx.userId ?? null);
20401
- record._setRaw("is_deleted", false);
20402
- if (opts.embedding !== void 0) {
20403
- record._setRaw("embedding", opts.embedding);
20404
- record._setRaw("embedding_model", opts.embeddingModel ?? null);
20405
- }
20406
- if (opts.sourceChunkIds !== void 0) {
20407
- record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
20408
- }
20409
- record._setRaw("proof_count", opts.proofCount ?? 1);
20410
- record._setRaw("source", opts.source ?? "manual");
20411
- if (opts.eventTime) {
20412
- record._setRaw("event_time_start", opts.eventTime.start ?? null);
20413
- record._setRaw("event_time_end", opts.eventTime.end ?? null);
20414
- record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
20415
- }
20416
- if (opts.factType !== void 0) {
20417
- record._setRaw("fact_type", opts.factType);
20418
- }
20419
- if (opts.trustTier !== void 0) {
20420
- record._setRaw("trust_tier", normalizeTrustTier(opts.trustTier));
20421
- }
20422
- record._setRaw("visibility", opts.visibility ?? "private");
20423
- record._setRaw(
20424
- "published_at",
20425
- opts.visibility && opts.visibility !== "private" ? opts.publishedAt ?? Date.now() : null
20426
- );
20427
- if (opts.geohash !== void 0) {
20428
- record._setRaw("geohash", opts.geohash);
20429
- }
20430
- });
20450
+ return ctx.vaultMemoryCollection.create(
20451
+ (record) => populateNewVaultMemory(record, ctx, opts, encryptedContent)
20452
+ );
20431
20453
  });
20432
20454
  return vaultMemoryToStored(created, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner);
20433
20455
  }
20434
20456
  async function createSupersedingMemoryOp(ctx, opts, targetId) {
20435
20457
  if (!targetId) return { created: null, retired: false };
20436
- const scope = opts.scope ?? "private";
20437
20458
  const encryptedContent = ctx.walletAddress && ctx.signMessage ? await encryptVaultMemoryContent(
20438
20459
  opts.content,
20439
20460
  ctx.walletAddress,
@@ -20451,27 +20472,9 @@ async function createSupersedingMemoryOp(ctx, opts, targetId) {
20451
20472
  return;
20452
20473
  }
20453
20474
  if (target.isDeleted || target.supersededBy || !isOwnedByCtxUser(ctx, target)) return;
20454
- createdRecord = await ctx.vaultMemoryCollection.create((record) => {
20455
- record._setRaw("content", encryptedContent);
20456
- record._setRaw("scope", scope);
20457
- record._setRaw("folder_id", opts.folderId ?? null);
20458
- record._setRaw("user_id", ctx.userId ?? null);
20459
- record._setRaw("is_deleted", false);
20460
- if (opts.embedding !== void 0) {
20461
- record._setRaw("embedding", opts.embedding);
20462
- record._setRaw("embedding_model", opts.embeddingModel ?? null);
20463
- }
20464
- if (opts.sourceChunkIds !== void 0) {
20465
- record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
20466
- }
20467
- record._setRaw("proof_count", opts.proofCount ?? 1);
20468
- record._setRaw("source", opts.source ?? "manual");
20469
- if (opts.eventTime) {
20470
- record._setRaw("event_time_start", opts.eventTime.start ?? null);
20471
- record._setRaw("event_time_end", opts.eventTime.end ?? null);
20472
- record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
20473
- }
20474
- });
20475
+ createdRecord = await ctx.vaultMemoryCollection.create(
20476
+ (record) => populateNewVaultMemory(record, ctx, opts, encryptedContent)
20477
+ );
20475
20478
  await target.update((r) => {
20476
20479
  r._setRaw("superseded_by", createdRecord.id);
20477
20480
  r._setRaw("superseded_at", Date.now());
@@ -20551,41 +20554,9 @@ async function createVaultMemoriesBatchOp(ctx, optionsArray) {
20551
20554
  if (ctx.canWrite && !await ctx.canWrite())
20552
20555
  throw new Error("Memory source is no longer eligible");
20553
20556
  const prepared = optionsArray.map(
20554
- (opts, i) => ctx.vaultMemoryCollection.prepareCreate((record) => {
20555
- record._setRaw("content", encryptedContents[i]);
20556
- record._setRaw("scope", opts.scope ?? "private");
20557
- record._setRaw("folder_id", opts.folderId ?? null);
20558
- record._setRaw("user_id", ctx.userId ?? null);
20559
- record._setRaw("is_deleted", false);
20560
- if (optionsArray[i].embedding !== void 0) {
20561
- record._setRaw("embedding", optionsArray[i].embedding);
20562
- record._setRaw("embedding_model", optionsArray[i].embeddingModel ?? null);
20563
- }
20564
- if (opts.sourceChunkIds !== void 0) {
20565
- record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
20566
- }
20567
- record._setRaw("proof_count", opts.proofCount ?? 1);
20568
- record._setRaw("source", opts.source ?? "manual");
20569
- if (opts.eventTime) {
20570
- record._setRaw("event_time_start", opts.eventTime.start ?? null);
20571
- record._setRaw("event_time_end", opts.eventTime.end ?? null);
20572
- record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
20573
- }
20574
- if (opts.factType !== void 0) {
20575
- record._setRaw("fact_type", opts.factType);
20576
- }
20577
- if (opts.trustTier !== void 0) {
20578
- record._setRaw("trust_tier", normalizeTrustTier(opts.trustTier));
20579
- }
20580
- record._setRaw("visibility", opts.visibility ?? "private");
20581
- record._setRaw(
20582
- "published_at",
20583
- opts.visibility && opts.visibility !== "private" ? opts.publishedAt ?? Date.now() : null
20584
- );
20585
- if (opts.geohash !== void 0) {
20586
- record._setRaw("geohash", opts.geohash);
20587
- }
20588
- })
20557
+ (opts, i) => ctx.vaultMemoryCollection.prepareCreate(
20558
+ (record) => populateNewVaultMemory(record, ctx, opts, encryptedContents[i])
20559
+ )
20589
20560
  );
20590
20561
  await ctx.database.batch(...prepared);
20591
20562
  return prepared;
@@ -21379,21 +21350,73 @@ async function hardDeleteDecayedOp(ctx, id, opts) {
21379
21350
  return false;
21380
21351
  }
21381
21352
  }
21382
- async function updateVaultMemoryEmbeddingOp(ctx, id, embedding, embeddingModel) {
21353
+ async function updateVaultMemoryEmbeddingOp(ctx, id, embedding, embeddingModel, expected) {
21383
21354
  try {
21384
21355
  const record = await ctx.vaultMemoryCollection.find(id);
21385
21356
  if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) return false;
21357
+ const readUpdatedAt = record.updatedAt.getTime();
21358
+ const readStoredContent = record._getRaw("content");
21359
+ if (expected?.updatedAt !== void 0 && expected.updatedAt !== readUpdatedAt) return false;
21360
+ if (expected?.content !== void 0) {
21361
+ const current = await vaultMemoryToStored(
21362
+ record,
21363
+ ctx.walletAddress,
21364
+ ctx.signMessage,
21365
+ ctx.embeddedWalletSigner
21366
+ );
21367
+ if (current.content !== expected.content) return false;
21368
+ }
21369
+ let written = false;
21386
21370
  await ctx.database.write(async () => {
21371
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) return;
21372
+ const originalUpdatedAt = record.updatedAt.getTime();
21373
+ if (originalUpdatedAt !== readUpdatedAt || record._getRaw("content") !== readStoredContent)
21374
+ return;
21387
21375
  await record.update((r) => {
21388
21376
  r._setRaw("embedding", embedding);
21389
21377
  r._setRaw("embedding_model", embeddingModel);
21378
+ r._setRaw("updated_at", originalUpdatedAt);
21390
21379
  });
21380
+ written = true;
21391
21381
  });
21392
- return true;
21382
+ return written;
21393
21383
  } catch {
21394
21384
  return false;
21395
21385
  }
21396
21386
  }
21387
+ function normalizeForDedupe(content) {
21388
+ return content.normalize("NFKC").trim().replace(/\s+/g, " ").toLowerCase();
21389
+ }
21390
+ async function findQuarantinedDuplicateOp(ctx, content, sourceIds, where) {
21391
+ const wanted = [...new Set(sourceIds)].sort().join("\0");
21392
+ const target = normalizeForDedupe(content);
21393
+ const rows = await ctx.vaultMemoryCollection.query(
21394
+ import_watermelondb18.Q.where("trust_tier", "quarantined"),
21395
+ import_watermelondb18.Q.where("is_deleted", false),
21396
+ import_watermelondb18.Q.where("scope", where.scope),
21397
+ import_watermelondb18.Q.where("folder_id", where.folderId),
21398
+ ...ctx.userId !== void 0 ? [import_watermelondb18.Q.where("user_id", ctx.userId)] : []
21399
+ ).unsafeFetchRaw();
21400
+ for (const raw of rows) {
21401
+ let ids;
21402
+ try {
21403
+ ids = JSON.parse(raw.source_chunk_ids ?? "[]");
21404
+ } catch {
21405
+ continue;
21406
+ }
21407
+ if (!Array.isArray(ids)) continue;
21408
+ const key = [...new Set(ids.filter((id) => typeof id === "string"))].sort().join("\0");
21409
+ if (key !== wanted) continue;
21410
+ const stored = await vaultMemoryRawToStored(
21411
+ raw,
21412
+ ctx.walletAddress,
21413
+ ctx.signMessage,
21414
+ ctx.embeddedWalletSigner
21415
+ );
21416
+ if (normalizeForDedupe(stored.content) === target) return stored.uniqueId;
21417
+ }
21418
+ return null;
21419
+ }
21397
21420
 
21398
21421
  // src/lib/memory/decay.ts
21399
21422
  var DAY_MS = 24 * 60 * 60 * 1e3;
@@ -21417,6 +21440,10 @@ var DEFAULT_DECAY_POLICY = {
21417
21440
  hardDeleteWindowMs: HARD_DELETE_WINDOW_MS,
21418
21441
  pastEventGraceMs: PAST_EVENT_GRACE_MS
21419
21442
  };
21443
+ function lastActivityAt(m) {
21444
+ const observedAt = Number.isFinite(m.lastObservedAt) ? m.lastObservedAt : m.updatedAt;
21445
+ return Math.max(m.updatedAt, observedAt);
21446
+ }
21420
21447
  function resolvePolicy(policy) {
21421
21448
  if (!policy) return DEFAULT_DECAY_POLICY;
21422
21449
  return {
@@ -21456,8 +21483,7 @@ function classifyDecay(m, now, policy) {
21456
21483
  if (m.eventTimeEnd < now - resolved.pastEventGraceMs) return "archive";
21457
21484
  if (m.eventTimeEnd >= now) return "keep";
21458
21485
  }
21459
- const observedAt = Number.isFinite(m.lastObservedAt) ? m.lastObservedAt : m.updatedAt;
21460
- if (now - Math.max(m.updatedAt, observedAt) > ttlForType(m.factType, resolved)) return "archive";
21486
+ if (now - lastActivityAt(m) > ttlForType(m.factType, resolved)) return "archive";
21461
21487
  return "keep";
21462
21488
  }
21463
21489
 
@@ -26996,6 +27022,9 @@ var MANUAL_FACT_TYPES = [
26996
27022
  function normalizeManualFactType(value) {
26997
27023
  return typeof value === "string" && MANUAL_FACT_TYPES.includes(value) ? value : void 0;
26998
27024
  }
27025
+ function isSharedScope(scope) {
27026
+ return scope === "shared";
27027
+ }
26999
27028
  async function notifyWritten(options, input, outcome) {
27000
27029
  try {
27001
27030
  await options?.onWritten?.({ input, outcome });
@@ -27064,14 +27093,17 @@ function createMemoryVaultTool(vaultCtx, options, embeddingOptions, cache) {
27064
27093
  try {
27065
27094
  const isUpdate = !!id;
27066
27095
  let previousContent;
27096
+ const scope = options?.scope ?? "private";
27067
27097
  if (isUpdate) {
27068
27098
  const existing = await getVaultMemoryOp(vaultCtx, id);
27069
27099
  if (!existing) {
27070
27100
  return `Error: Memory with ID "${id}" not found. Creating a new memory instead would require a separate call without an ID.`;
27071
27101
  }
27102
+ if (isSharedScope(existing.scope) !== isSharedScope(scope)) {
27103
+ 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.`;
27104
+ }
27072
27105
  previousContent = existing.content;
27073
27106
  }
27074
- const scope = options?.scope ?? "private";
27075
27107
  const operation = {
27076
27108
  action: isUpdate ? "update" : "add",
27077
27109
  content,
@@ -28271,6 +28303,24 @@ async function extractAndRetain(messages, retainCtx, options) {
28271
28303
  const tRetain = Date.now();
28272
28304
  for (const { candidate, isQuarantined, reason, signature } of toRetain) {
28273
28305
  try {
28306
+ if (isQuarantined) {
28307
+ const existingId = await findQuarantinedDuplicateOp(
28308
+ retainCtx.vaultCtx,
28309
+ candidate.content,
28310
+ candidate.sourceMessageIds,
28311
+ // The same defaults retain() writes the row with.
28312
+ { scope: options.scope ?? "private", folderId: options.folderId ?? null }
28313
+ ).catch(() => null);
28314
+ if (existingId) {
28315
+ quarantinedInfo.push({
28316
+ candidate,
28317
+ memoryId: existingId,
28318
+ reason,
28319
+ signature
28320
+ });
28321
+ continue;
28322
+ }
28323
+ }
28274
28324
  const result = await retain(candidate.content, retainCtx, {
28275
28325
  source: "auto-extracted",
28276
28326
  sourceChunkIds: candidate.sourceMessageIds,
@@ -28716,7 +28766,7 @@ function createLlmDecayClassifier(options) {
28716
28766
  if (!content || content.trim().length === 0) return ruleVerdict;
28717
28767
  const trimmed = content.trim().slice(0, MAX_CONTENT_CHARS);
28718
28768
  const safe = redactor ? (await redactor.redactTextAsync(trimmed)).text : trimmed;
28719
- const meta = `factType: ${input.factType ?? "none"}; ageDays: ${ageDays(input.updatedAt, now)}`;
28769
+ const meta = `factType: ${input.factType ?? "none"}; ageDays: ${ageDays(lastActivityAt(input), now)}`;
28720
28770
  let parsed;
28721
28771
  try {
28722
28772
  parsed = await callPortalJsonCompletion({
@@ -28749,9 +28799,9 @@ Should this be kept or archived?`,
28749
28799
  }
28750
28800
  };
28751
28801
  }
28752
- function ageDays(updatedAt, now) {
28753
- if (!Number.isFinite(updatedAt) || !Number.isFinite(now)) return 0;
28754
- return Math.max(0, Math.floor((now - updatedAt) / (24 * 60 * 60 * 1e3)));
28802
+ function ageDays(activityAt, now) {
28803
+ if (!Number.isFinite(activityAt) || !Number.isFinite(now)) return 0;
28804
+ return Math.max(0, Math.floor((now - activityAt) / (24 * 60 * 60 * 1e3)));
28755
28805
  }
28756
28806
  function parseVerdict(parsed) {
28757
28807
  if (typeof parsed !== "object" || parsed === null) return null;
@@ -28804,7 +28854,7 @@ function createDecaySweeper(options) {
28804
28854
  if (!classifier || !isBorderline(input)) return ruleVerdict;
28805
28855
  if (input.id) {
28806
28856
  const cached = classifierCache.get(input.id);
28807
- if (cached && cached.updatedAt === input.updatedAt) return cached.verdict;
28857
+ if (cached && cached.activityAt === lastActivityAt(input)) return cached.verdict;
28808
28858
  }
28809
28859
  if (sweep.classifierCalls >= maxClassifierCalls) {
28810
28860
  if (!sweep.ceilingLogged) {
@@ -28818,7 +28868,7 @@ function createDecaySweeper(options) {
28818
28868
  sweep.classifierCalls++;
28819
28869
  try {
28820
28870
  const verdict = await classifier.classify(input, ruleVerdict, now);
28821
- if (input.id) classifierCache.set(input.id, { updatedAt: input.updatedAt, verdict });
28871
+ if (input.id) classifierCache.set(input.id, { activityAt: lastActivityAt(input), verdict });
28822
28872
  return verdict;
28823
28873
  } catch (err) {
28824
28874
  getLogger().warn(
@@ -29560,7 +29610,10 @@ function reviewedMemoryIdsSignature(ids) {
29560
29610
  return [...new Set(ids)].sort().join("\n");
29561
29611
  }
29562
29612
  function facetsSignature(facets) {
29563
- return facets.map((f) => JSON.stringify([f.key, f.label, f.query, f.guidance, facetResponseSchema(f.key)])).sort().join("\n");
29613
+ return [
29614
+ JSON.stringify(FACET_SYSTEM_PROMPT),
29615
+ ...facets.map((f) => JSON.stringify([f.key, f.label, f.query, f.guidance, facetResponseSchema(f.key)])).sort()
29616
+ ].join("\n");
29564
29617
  }
29565
29618
  function trendsEqual(a, b) {
29566
29619
  if (!a) return false;
@@ -29782,7 +29835,7 @@ var FACET_SYSTEM_PROMPT = `You are writing one section of a person's shareable p
29782
29835
  Rules:
29783
29836
  - Ground every claim in the supplied memories \u2014 never invent, infer beyond, or embellish what they support.
29784
29837
  - If the memories don't cover this section, return an empty summary with hasEvidence=false. Do not pad or guess.
29785
- - Write in third person about the person, in a natural voice suitable for a public profile (no "I"/"you", no name repetition).
29838
+ - 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.
29786
29839
  - Be concise and specific; no preamble, hedging, or meta-commentary.
29787
29840
  - Respond as JSON in exactly the shape the user turn states, with no extra fields.`;
29788
29841
  function buildFacetUserInstructions(facet) {
@@ -30451,6 +30504,37 @@ async function assembleMemoryContext(options) {
30451
30504
  var import_watermelondb24 = __toESM(require_watermelondb());
30452
30505
  var MAX_JOBS_PER_PASS = 3;
30453
30506
  var MAX_ATTEMPTS = 3;
30507
+ var MAX_FAILED_SESSIONS = 3;
30508
+ var MIN_FAILED_SESSION_GAP_MS = 60 * 60 * 1e3;
30509
+ var REQUEST_REJECTED_STATUSES = /* @__PURE__ */ new Set([400, 404, 413, 422]);
30510
+ var CONTENT_FAILURE_REASONS = /* @__PURE__ */ new Set([
30511
+ "empty-content",
30512
+ "invalid-json",
30513
+ "null-completion",
30514
+ "body-parse-failed"
30515
+ ]);
30516
+ var BACKOFF_ALLOWANCE_MS = 2100;
30517
+ var RETAIN_BUDGET_MS = 12e4;
30518
+ function defaultBatchTimeoutMs(extract) {
30519
+ const attempts = Math.max(1, extract.maxAttempts ?? 3);
30520
+ const calls = (extract.timeoutMs ?? 6e4) * attempts + BACKOFF_ALLOWANCE_MS * (attempts - 1);
30521
+ const extraction = extract.totalTimeoutMs !== void 0 ? Math.min(extract.totalTimeoutMs, calls) : calls;
30522
+ return extraction + RETAIN_BUDGET_MS;
30523
+ }
30524
+ var BatchFailureError = class extends Error {
30525
+ constructor(message, terminal) {
30526
+ super(message);
30527
+ this.terminal = terminal;
30528
+ }
30529
+ };
30530
+ var AccountFailureError = class extends Error {
30531
+ };
30532
+ var LockedSourcesError = class extends Error {
30533
+ constructor(persistentIds) {
30534
+ super("Source messages are locked; retained for retry");
30535
+ this.persistentIds = persistentIds;
30536
+ }
30537
+ };
30454
30538
  function idsOf(job) {
30455
30539
  return JSON.parse(String(job._getRaw("message_ids")));
30456
30540
  }
@@ -30458,6 +30542,16 @@ function seqOf(job) {
30458
30542
  const raw = job._getRaw("watermark_seq");
30459
30543
  return typeof raw === "number" && raw > 0 ? raw : void 0;
30460
30544
  }
30545
+ function failedSessionsOf(job, head) {
30546
+ if (job._getRaw("failed_head") !== head) return 0;
30547
+ const raw = job._getRaw("failed_sessions");
30548
+ return typeof raw === "number" && raw > 0 ? raw : 0;
30549
+ }
30550
+ function failedAtOf(job, head) {
30551
+ if (job._getRaw("failed_head") !== head) return void 0;
30552
+ const raw = job._getRaw("failed_at");
30553
+ return typeof raw === "number" ? raw : void 0;
30554
+ }
30461
30555
  function createDurableAutoExtractor(options) {
30462
30556
  const vault = options.retainCtx.vaultCtx;
30463
30557
  const database = vault.database;
@@ -30480,6 +30574,9 @@ function createDurableAutoExtractor(options) {
30480
30574
  let writes = Promise.resolve();
30481
30575
  let storeFailures = 0;
30482
30576
  const attempts = /* @__PURE__ */ new Map();
30577
+ const sessionFailures = /* @__PURE__ */ new Map();
30578
+ const skippedThisSession = /* @__PURE__ */ new Set();
30579
+ const clock = options.now ?? Date.now;
30483
30580
  const reportError = (error, conversationId) => {
30484
30581
  try {
30485
30582
  options.onError?.(error instanceof Error ? error : new Error(String(error)), conversationId);
@@ -30508,6 +30605,7 @@ function createDurableAutoExtractor(options) {
30508
30605
  }
30509
30606
  async function extractBatch(messages, conversationId, ids, jobScope, jobFolderId) {
30510
30607
  let watchdog;
30608
+ let cancelled = false;
30511
30609
  const model = options.modelForScope?.(jobScope) ?? options.extract.model;
30512
30610
  try {
30513
30611
  return await Promise.race([
@@ -30518,7 +30616,7 @@ function createDurableAutoExtractor(options) {
30518
30616
  ...options.retainCtx,
30519
30617
  vaultCtx: {
30520
30618
  ...vault,
30521
- canWrite: async () => !disposed && await storage.conversationsCollection.query(import_watermelondb24.Q.where("conversation_id", conversationId), import_watermelondb24.Q.where("is_deleted", false)).fetchCount() > 0 && await storage.messagesCollection.query(import_watermelondb24.Q.where("conversation_id", conversationId), import_watermelondb24.Q.where("id", import_watermelondb24.Q.oneOf(ids))).fetchCount() === ids.length && (!vault.canWrite || await vault.canWrite()) && !disposed
30619
+ canWrite: async () => !disposed && !cancelled && await storage.conversationsCollection.query(import_watermelondb24.Q.where("conversation_id", conversationId), import_watermelondb24.Q.where("is_deleted", false)).fetchCount() > 0 && await storage.messagesCollection.query(import_watermelondb24.Q.where("conversation_id", conversationId), import_watermelondb24.Q.where("id", import_watermelondb24.Q.oneOf(ids))).fetchCount() === ids.length && (!vault.canWrite || await vault.canWrite()) && !disposed && !cancelled
30522
30620
  }
30523
30621
  },
30524
30622
  // The model follows the job's scope, not the mode alive at drain time.
@@ -30538,14 +30636,116 @@ function createDurableAutoExtractor(options) {
30538
30636
  new Promise((_resolve, reject) => {
30539
30637
  watchdog = setTimeout(
30540
30638
  () => reject(new Error("Extraction did not settle in time; retained for retry")),
30541
- options.batchTimeoutMs ?? 18e4
30639
+ options.batchTimeoutMs ?? defaultBatchTimeoutMs(options.extract)
30542
30640
  );
30543
30641
  })
30544
30642
  ]);
30545
30643
  } finally {
30644
+ cancelled = true;
30546
30645
  if (watchdog !== void 0) clearTimeout(watchdog);
30547
30646
  }
30548
30647
  }
30648
+ async function acknowledge(job, ids, loaded, abandoned) {
30649
+ attempts.delete(job.id);
30650
+ sessionFailures.delete(job.id);
30651
+ skippedThisSession.delete(job.id);
30652
+ const sequences = /* @__PURE__ */ new Map();
30653
+ for (const message of loaded)
30654
+ if (message && message.messageId > 0) sequences.set(message.uniqueId, message.messageId);
30655
+ await database.write(async () => {
30656
+ const current = idsOf(job);
30657
+ const acknowledged = ids.filter((id) => current.includes(id));
30658
+ if (!acknowledged.length) return;
30659
+ const done = new Set(acknowledged);
30660
+ const remaining = current.filter((id) => !done.has(id));
30661
+ await job.update((r) => {
30662
+ r._setRaw(
30663
+ "message_ids",
30664
+ JSON.stringify(
30665
+ remaining.length ? [...abandoned ? [] : acknowledged.slice(-2), ...remaining] : []
30666
+ )
30667
+ );
30668
+ const newest = acknowledged[acknowledged.length - 1];
30669
+ r._setRaw("watermark", newest);
30670
+ const sequence = sequences.get(newest);
30671
+ if (sequence !== void 0) {
30672
+ const prior = seqOf(job);
30673
+ r._setRaw("watermark_seq", prior === void 0 ? sequence : Math.max(prior, sequence));
30674
+ }
30675
+ r._setRaw("failed_sessions", abandoned ? MAX_FAILED_SESSIONS : null);
30676
+ r._setRaw("failed_head", abandoned ? newest : null);
30677
+ r._setRaw("failed_at", null);
30678
+ });
30679
+ });
30680
+ }
30681
+ async function recordFailure(job, ids, loaded, error, conversationId) {
30682
+ const head = ids[0];
30683
+ if (!head || !loaded.some(Boolean)) return false;
30684
+ if (error instanceof AccountFailureError) {
30685
+ skippedThisSession.add(job.id);
30686
+ return false;
30687
+ }
30688
+ const counted = error instanceof BatchFailureError || error instanceof LockedSourcesError && error.persistentIds.length > 0;
30689
+ if (!counted) return false;
30690
+ const prior = sessionFailures.get(job.id);
30691
+ const count = prior?.head === head ? prior.count + 1 : 1;
30692
+ sessionFailures.set(job.id, { head, count });
30693
+ const terminal = error instanceof BatchFailureError && error.terminal;
30694
+ if (!terminal && count < MAX_ATTEMPTS) return false;
30695
+ skippedThisSession.add(job.id);
30696
+ const now = clock();
30697
+ const lastCounted = failedAtOf(job, head);
30698
+ if (lastCounted !== void 0 && now - lastCounted < MIN_FAILED_SESSION_GAP_MS) return false;
30699
+ const failures = failedSessionsOf(job, head) + 1;
30700
+ if (failures < MAX_FAILED_SESSIONS) {
30701
+ await database.write(
30702
+ () => job.update((r) => {
30703
+ r._setRaw("failed_sessions", failures);
30704
+ r._setRaw("failed_head", head);
30705
+ r._setRaw("failed_at", now);
30706
+ })
30707
+ );
30708
+ return false;
30709
+ }
30710
+ if (error instanceof LockedSourcesError) {
30711
+ const dropped = new Set(error.persistentIds);
30712
+ let newest;
30713
+ for (const message of loaded)
30714
+ if (message && dropped.has(message.uniqueId) && message.messageId > (newest?.seq ?? 0))
30715
+ newest = { id: message.uniqueId, seq: message.messageId };
30716
+ const advance = newest !== void 0 && newest.seq > (seqOf(job) ?? 0) ? newest : void 0;
30717
+ await database.write(
30718
+ () => job.update((r) => {
30719
+ r._setRaw("message_ids", JSON.stringify(idsOf(job).filter((id) => !dropped.has(id))));
30720
+ if (advance) {
30721
+ r._setRaw("watermark", advance.id);
30722
+ r._setRaw("watermark_seq", advance.seq);
30723
+ }
30724
+ r._setRaw("failed_sessions", advance ? MAX_FAILED_SESSIONS : null);
30725
+ r._setRaw("failed_head", advance ? advance.id : null);
30726
+ r._setRaw("failed_at", null);
30727
+ })
30728
+ );
30729
+ attempts.delete(job.id);
30730
+ sessionFailures.delete(job.id);
30731
+ skippedThisSession.delete(job.id);
30732
+ reportError(
30733
+ new Error(
30734
+ `Dropped ${dropped.size} source id(s) that stayed locked for ${failures} sessions to unblock extraction`
30735
+ ),
30736
+ conversationId
30737
+ );
30738
+ return true;
30739
+ }
30740
+ await acknowledge(job, ids, loaded, true);
30741
+ reportError(
30742
+ new Error(
30743
+ `Abandoned an extraction batch of ${ids.length} source id(s) after it failed in ${failures} sessions`
30744
+ ),
30745
+ conversationId
30746
+ );
30747
+ return true;
30748
+ }
30549
30749
  async function drain() {
30550
30750
  if (disposed || running) return;
30551
30751
  running = true;
@@ -30560,21 +30760,23 @@ function createDurableAutoExtractor(options) {
30560
30760
  for (const job of pending) {
30561
30761
  if (disposed) break;
30562
30762
  const attempted = attempts.get(job.id) ?? 0;
30563
- if (attempted >= MAX_ATTEMPTS) continue;
30763
+ if (attempted >= MAX_ATTEMPTS || skippedThisSession.has(job.id)) continue;
30564
30764
  if (processed >= MAX_JOBS_PER_PASS) {
30565
30765
  scheduleAfterSuccess = true;
30566
30766
  break;
30567
30767
  }
30568
30768
  processed++;
30569
30769
  const conversationId = String(job._getRaw("conversation_id"));
30770
+ let ids = [];
30771
+ let loaded = [];
30570
30772
  try {
30571
30773
  const conversations = await storage.conversationsCollection.query(import_watermelondb24.Q.where("conversation_id", conversationId), import_watermelondb24.Q.where("is_deleted", false)).fetchCount();
30572
30774
  if (!conversations) {
30573
30775
  await database.write(() => job.destroyPermanently());
30574
30776
  continue;
30575
30777
  }
30576
- const ids = idsOf(job).slice(0, 20);
30577
- const loaded = await Promise.all(ids.map((id) => getMessageOp(storage, id)));
30778
+ ids = idsOf(job).slice(0, 20);
30779
+ loaded = await Promise.all(ids.map((id) => getMessageOp(storage, id)));
30578
30780
  const missing = ids.filter((_id, index) => loaded[index] === null);
30579
30781
  if (missing.length) {
30580
30782
  if (attempted < MAX_ATTEMPTS - 1)
@@ -30598,17 +30800,21 @@ function createDurableAutoExtractor(options) {
30598
30800
  );
30599
30801
  continue;
30600
30802
  }
30601
- if (loaded.some(
30602
- (message) => message && (message.decryptionStatus || isEncrypted(message.content))
30603
- )) {
30604
- throw new Error("Source messages are locked; retained for retry");
30803
+ const locked = loaded.filter((message) => message && isEncrypted(message.content));
30804
+ if (locked.length) {
30805
+ throw new LockedSourcesError(
30806
+ locked.flatMap(
30807
+ (message) => message.decryptionStatus === "auth_mismatch" || message.decryptionStatus === "invalid_payload" ? [message.uniqueId] : []
30808
+ )
30809
+ );
30605
30810
  }
30606
30811
  const messages = loaded.flatMap(
30607
30812
  (m) => m && m.conversationId === conversationId && (m.role === "user" || m.role === "assistant") ? [{ id: m.uniqueId, role: m.role, content: m.content }] : []
30608
30813
  );
30609
30814
  let success = messages.length === 0;
30815
+ let result;
30610
30816
  if (messages.length) {
30611
- const result = await extractBatch(
30817
+ result = await extractBatch(
30612
30818
  messages,
30613
30819
  conversationId,
30614
30820
  ids,
@@ -30618,41 +30824,35 @@ function createDurableAutoExtractor(options) {
30618
30824
  success = result.failedCount === 0 && result.outcome !== "empty-after-retry";
30619
30825
  options.onTurnComplete?.(result);
30620
30826
  }
30621
- if (!success) throw new Error("Extraction batch incomplete; retained for retry");
30622
- attempts.delete(job.id);
30623
- const sequences = /* @__PURE__ */ new Map();
30624
- for (const message of loaded)
30625
- if (message && message.messageId > 0)
30626
- sequences.set(message.uniqueId, message.messageId);
30627
- await database.write(async () => {
30628
- const current = idsOf(job);
30629
- const acknowledged = ids.filter((id) => current.includes(id));
30630
- if (!acknowledged.length) return;
30631
- const done = new Set(acknowledged);
30632
- const remaining = current.filter((id) => !done.has(id));
30633
- if (remaining.length) {
30634
- await job.update(
30635
- (r) => r._setRaw("message_ids", JSON.stringify([...acknowledged.slice(-2), ...remaining]))
30636
- );
30637
- } else await job.update((r) => r._setRaw("message_ids", "[]"));
30638
- const newest = acknowledged[acknowledged.length - 1];
30639
- await job.update((r) => {
30640
- r._setRaw("watermark", newest);
30641
- const sequence = sequences.get(newest);
30642
- if (sequence !== void 0) {
30643
- const prior = seqOf(job);
30644
- r._setRaw(
30645
- "watermark_seq",
30646
- prior === void 0 ? sequence : Math.max(prior, sequence)
30827
+ if (!success) {
30828
+ const failure = result?.failure;
30829
+ if (failure?.reason === "http-terminal") {
30830
+ const status = failure.httpStatus;
30831
+ if (status !== void 0 && REQUEST_REJECTED_STATUSES.has(status))
30832
+ throw new BatchFailureError(
30833
+ `Extraction rejected the batch (HTTP ${status}); not retried this session`,
30834
+ true
30647
30835
  );
30648
- }
30649
- });
30650
- });
30836
+ throw new AccountFailureError(
30837
+ `Extraction refused for the account (HTTP ${status ?? "unknown"}); retried next session`
30838
+ );
30839
+ }
30840
+ if ((result?.failedCount ?? 0) > 0 || failure !== void 0 && CONTENT_FAILURE_REASONS.has(failure.reason))
30841
+ throw new BatchFailureError("Extraction batch incomplete; retained for retry", false);
30842
+ throw new Error("Extraction batch incomplete; retained for retry");
30843
+ }
30844
+ await acknowledge(job, ids, loaded, false);
30651
30845
  scheduleAfterSuccess = true;
30652
30846
  } catch (error) {
30653
30847
  attempts.set(job.id, attempted + 1);
30654
30848
  retry = true;
30655
30849
  reportError(error, conversationId);
30850
+ try {
30851
+ if (await recordFailure(job, ids, loaded, error, conversationId))
30852
+ scheduleAfterSuccess = true;
30853
+ } catch (recordError) {
30854
+ reportError(recordError, conversationId);
30855
+ }
30656
30856
  }
30657
30857
  }
30658
30858
  } catch (error) {
@@ -30722,7 +30922,7 @@ function createDurableAutoExtractor(options) {
30722
30922
  const start = boundary >= 0 ? boundary + 1 : Math.max(0, ids.length - Math.max(1, options.windowSize ?? 6));
30723
30923
  if (start >= ids.length) return;
30724
30924
  const ownWatermark = job?._getRaw("watermark");
30725
- const atOwnWatermark = !!job && boundary >= 0 && (ids[boundary] === ownWatermark || sequences.get(ids[boundary]) !== void 0 && sequences.get(ids[boundary]) === seqOf(job));
30925
+ 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));
30726
30926
  const overlapStart = atOwnWatermark ? Math.max(0, boundary - 1) : start;
30727
30927
  const fresh = ids.slice(overlapStart).filter((id) => !foreignSources.has(id));
30728
30928
  if (!fresh.length) return;