@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.
@@ -17129,7 +17129,7 @@ __decorateClass([
17129
17129
  ], VaultFolder.prototype, "context", 2);
17130
17130
 
17131
17131
  // src/lib/db/schema.ts
17132
- var SDK_SCHEMA_VERSION = 46;
17132
+ var SDK_SCHEMA_VERSION = 47;
17133
17133
  var sdkSchema = (0, import_watermelondb13.appSchema)({
17134
17134
  version: SDK_SCHEMA_VERSION,
17135
17135
  tables: [
@@ -17145,7 +17145,11 @@ var sdkSchema = (0, import_watermelondb13.appSchema)({
17145
17145
  // source. `watermark` alone is an id, and deleting that message erased
17146
17146
  // the anchor — see the column note in extractionJobs/models.ts.
17147
17147
  { name: "watermark_seq", type: "number", isOptional: true },
17148
- { name: "folder_id", type: "string", isOptional: true }
17148
+ { name: "folder_id", type: "string", isOptional: true },
17149
+ // Poison accounting for the head batch; see extractionJobs/models.ts.
17150
+ { name: "failed_sessions", type: "number", isOptional: true },
17151
+ { name: "failed_head", type: "string", isOptional: true },
17152
+ { name: "failed_at", type: "number", isOptional: true }
17149
17153
  ]
17150
17154
  }),
17151
17155
  // Chat storage tables
@@ -18169,6 +18173,22 @@ var sdkMigrations = (0, import_migrations.schemaMigrations)({
18169
18173
  ]
18170
18174
  })
18171
18175
  ]
18176
+ },
18177
+ // v46 -> v47: persisted poison count (and when it was last counted) for a
18178
+ // durable-extraction job's head batch. NULL = never failed, which is right
18179
+ // for every existing row.
18180
+ {
18181
+ toVersion: 47,
18182
+ steps: [
18183
+ (0, import_migrations.addColumns)({
18184
+ table: "memory_extraction_jobs",
18185
+ columns: [
18186
+ { name: "failed_sessions", type: "number", isOptional: true },
18187
+ { name: "failed_head", type: "string", isOptional: true },
18188
+ { name: "failed_at", type: "number", isOptional: true }
18189
+ ]
18190
+ })
18191
+ ]
18172
18192
  }
18173
18193
  ]
18174
18194
  });
@@ -22065,8 +22085,42 @@ async function vaultMemoryToStored(memory, walletAddress, signMessage, embeddedW
22065
22085
  }
22066
22086
  return raw;
22067
22087
  }
22088
+ function populateNewVaultMemory(record, ctx, opts, encryptedContent) {
22089
+ record._setRaw("content", encryptedContent);
22090
+ record._setRaw("scope", opts.scope ?? "private");
22091
+ record._setRaw("folder_id", opts.folderId ?? null);
22092
+ record._setRaw("user_id", ctx.userId ?? null);
22093
+ record._setRaw("is_deleted", false);
22094
+ if (opts.embedding !== void 0) {
22095
+ record._setRaw("embedding", opts.embedding);
22096
+ record._setRaw("embedding_model", opts.embeddingModel ?? null);
22097
+ }
22098
+ if (opts.sourceChunkIds !== void 0) {
22099
+ record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
22100
+ }
22101
+ record._setRaw("proof_count", opts.proofCount ?? 1);
22102
+ record._setRaw("source", opts.source ?? "manual");
22103
+ if (opts.eventTime) {
22104
+ record._setRaw("event_time_start", opts.eventTime.start ?? null);
22105
+ record._setRaw("event_time_end", opts.eventTime.end ?? null);
22106
+ record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
22107
+ }
22108
+ if (opts.factType !== void 0) {
22109
+ record._setRaw("fact_type", opts.factType);
22110
+ }
22111
+ if (opts.trustTier !== void 0) {
22112
+ record._setRaw("trust_tier", normalizeTrustTier(opts.trustTier));
22113
+ }
22114
+ record._setRaw("visibility", opts.visibility ?? "private");
22115
+ record._setRaw(
22116
+ "published_at",
22117
+ opts.visibility && opts.visibility !== "private" ? opts.publishedAt ?? Date.now() : null
22118
+ );
22119
+ if (opts.geohash !== void 0) {
22120
+ record._setRaw("geohash", opts.geohash);
22121
+ }
22122
+ }
22068
22123
  async function createVaultMemoryOp(ctx, opts) {
22069
- const scope = opts.scope ?? "private";
22070
22124
  const encryptedContent = ctx.walletAddress && ctx.signMessage ? await encryptVaultMemoryContent(
22071
22125
  opts.content,
22072
22126
  ctx.walletAddress,
@@ -22076,47 +22130,14 @@ async function createVaultMemoryOp(ctx, opts) {
22076
22130
  const created = await ctx.database.write(async () => {
22077
22131
  if (ctx.canWrite && !await ctx.canWrite())
22078
22132
  throw new Error("Memory source is no longer eligible");
22079
- return ctx.vaultMemoryCollection.create((record) => {
22080
- record._setRaw("content", encryptedContent);
22081
- record._setRaw("scope", scope);
22082
- record._setRaw("folder_id", opts.folderId ?? null);
22083
- record._setRaw("user_id", ctx.userId ?? null);
22084
- record._setRaw("is_deleted", false);
22085
- if (opts.embedding !== void 0) {
22086
- record._setRaw("embedding", opts.embedding);
22087
- record._setRaw("embedding_model", opts.embeddingModel ?? null);
22088
- }
22089
- if (opts.sourceChunkIds !== void 0) {
22090
- record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
22091
- }
22092
- record._setRaw("proof_count", opts.proofCount ?? 1);
22093
- record._setRaw("source", opts.source ?? "manual");
22094
- if (opts.eventTime) {
22095
- record._setRaw("event_time_start", opts.eventTime.start ?? null);
22096
- record._setRaw("event_time_end", opts.eventTime.end ?? null);
22097
- record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
22098
- }
22099
- if (opts.factType !== void 0) {
22100
- record._setRaw("fact_type", opts.factType);
22101
- }
22102
- if (opts.trustTier !== void 0) {
22103
- record._setRaw("trust_tier", normalizeTrustTier(opts.trustTier));
22104
- }
22105
- record._setRaw("visibility", opts.visibility ?? "private");
22106
- record._setRaw(
22107
- "published_at",
22108
- opts.visibility && opts.visibility !== "private" ? opts.publishedAt ?? Date.now() : null
22109
- );
22110
- if (opts.geohash !== void 0) {
22111
- record._setRaw("geohash", opts.geohash);
22112
- }
22113
- });
22133
+ return ctx.vaultMemoryCollection.create(
22134
+ (record) => populateNewVaultMemory(record, ctx, opts, encryptedContent)
22135
+ );
22114
22136
  });
22115
22137
  return vaultMemoryToStored(created, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner);
22116
22138
  }
22117
22139
  async function createSupersedingMemoryOp(ctx, opts, targetId) {
22118
22140
  if (!targetId) return { created: null, retired: false };
22119
- const scope = opts.scope ?? "private";
22120
22141
  const encryptedContent = ctx.walletAddress && ctx.signMessage ? await encryptVaultMemoryContent(
22121
22142
  opts.content,
22122
22143
  ctx.walletAddress,
@@ -22134,27 +22155,9 @@ async function createSupersedingMemoryOp(ctx, opts, targetId) {
22134
22155
  return;
22135
22156
  }
22136
22157
  if (target.isDeleted || target.supersededBy || !isOwnedByCtxUser(ctx, target)) return;
22137
- createdRecord = await ctx.vaultMemoryCollection.create((record) => {
22138
- record._setRaw("content", encryptedContent);
22139
- record._setRaw("scope", scope);
22140
- record._setRaw("folder_id", opts.folderId ?? null);
22141
- record._setRaw("user_id", ctx.userId ?? null);
22142
- record._setRaw("is_deleted", false);
22143
- if (opts.embedding !== void 0) {
22144
- record._setRaw("embedding", opts.embedding);
22145
- record._setRaw("embedding_model", opts.embeddingModel ?? null);
22146
- }
22147
- if (opts.sourceChunkIds !== void 0) {
22148
- record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
22149
- }
22150
- record._setRaw("proof_count", opts.proofCount ?? 1);
22151
- record._setRaw("source", opts.source ?? "manual");
22152
- if (opts.eventTime) {
22153
- record._setRaw("event_time_start", opts.eventTime.start ?? null);
22154
- record._setRaw("event_time_end", opts.eventTime.end ?? null);
22155
- record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
22156
- }
22157
- });
22158
+ createdRecord = await ctx.vaultMemoryCollection.create(
22159
+ (record) => populateNewVaultMemory(record, ctx, opts, encryptedContent)
22160
+ );
22158
22161
  await target.update((r) => {
22159
22162
  r._setRaw("superseded_by", createdRecord.id);
22160
22163
  r._setRaw("superseded_at", Date.now());
@@ -22234,41 +22237,9 @@ async function createVaultMemoriesBatchOp(ctx, optionsArray) {
22234
22237
  if (ctx.canWrite && !await ctx.canWrite())
22235
22238
  throw new Error("Memory source is no longer eligible");
22236
22239
  const prepared = optionsArray.map(
22237
- (opts, i) => ctx.vaultMemoryCollection.prepareCreate((record) => {
22238
- record._setRaw("content", encryptedContents[i]);
22239
- record._setRaw("scope", opts.scope ?? "private");
22240
- record._setRaw("folder_id", opts.folderId ?? null);
22241
- record._setRaw("user_id", ctx.userId ?? null);
22242
- record._setRaw("is_deleted", false);
22243
- if (optionsArray[i].embedding !== void 0) {
22244
- record._setRaw("embedding", optionsArray[i].embedding);
22245
- record._setRaw("embedding_model", optionsArray[i].embeddingModel ?? null);
22246
- }
22247
- if (opts.sourceChunkIds !== void 0) {
22248
- record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
22249
- }
22250
- record._setRaw("proof_count", opts.proofCount ?? 1);
22251
- record._setRaw("source", opts.source ?? "manual");
22252
- if (opts.eventTime) {
22253
- record._setRaw("event_time_start", opts.eventTime.start ?? null);
22254
- record._setRaw("event_time_end", opts.eventTime.end ?? null);
22255
- record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
22256
- }
22257
- if (opts.factType !== void 0) {
22258
- record._setRaw("fact_type", opts.factType);
22259
- }
22260
- if (opts.trustTier !== void 0) {
22261
- record._setRaw("trust_tier", normalizeTrustTier(opts.trustTier));
22262
- }
22263
- record._setRaw("visibility", opts.visibility ?? "private");
22264
- record._setRaw(
22265
- "published_at",
22266
- opts.visibility && opts.visibility !== "private" ? opts.publishedAt ?? Date.now() : null
22267
- );
22268
- if (opts.geohash !== void 0) {
22269
- record._setRaw("geohash", opts.geohash);
22270
- }
22271
- })
22240
+ (opts, i) => ctx.vaultMemoryCollection.prepareCreate(
22241
+ (record) => populateNewVaultMemory(record, ctx, opts, encryptedContents[i])
22242
+ )
22272
22243
  );
22273
22244
  await ctx.database.batch(...prepared);
22274
22245
  return prepared;
@@ -23062,21 +23033,73 @@ async function hardDeleteDecayedOp(ctx, id, opts) {
23062
23033
  return false;
23063
23034
  }
23064
23035
  }
23065
- async function updateVaultMemoryEmbeddingOp(ctx, id, embedding, embeddingModel) {
23036
+ async function updateVaultMemoryEmbeddingOp(ctx, id, embedding, embeddingModel, expected) {
23066
23037
  try {
23067
23038
  const record = await ctx.vaultMemoryCollection.find(id);
23068
23039
  if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) return false;
23040
+ const readUpdatedAt = record.updatedAt.getTime();
23041
+ const readStoredContent = record._getRaw("content");
23042
+ if (expected?.updatedAt !== void 0 && expected.updatedAt !== readUpdatedAt) return false;
23043
+ if (expected?.content !== void 0) {
23044
+ const current = await vaultMemoryToStored(
23045
+ record,
23046
+ ctx.walletAddress,
23047
+ ctx.signMessage,
23048
+ ctx.embeddedWalletSigner
23049
+ );
23050
+ if (current.content !== expected.content) return false;
23051
+ }
23052
+ let written = false;
23069
23053
  await ctx.database.write(async () => {
23054
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) return;
23055
+ const originalUpdatedAt = record.updatedAt.getTime();
23056
+ if (originalUpdatedAt !== readUpdatedAt || record._getRaw("content") !== readStoredContent)
23057
+ return;
23070
23058
  await record.update((r) => {
23071
23059
  r._setRaw("embedding", embedding);
23072
23060
  r._setRaw("embedding_model", embeddingModel);
23061
+ r._setRaw("updated_at", originalUpdatedAt);
23073
23062
  });
23063
+ written = true;
23074
23064
  });
23075
- return true;
23065
+ return written;
23076
23066
  } catch {
23077
23067
  return false;
23078
23068
  }
23079
23069
  }
23070
+ function normalizeForDedupe(content) {
23071
+ return content.normalize("NFKC").trim().replace(/\s+/g, " ").toLowerCase();
23072
+ }
23073
+ async function findQuarantinedDuplicateOp(ctx, content, sourceIds, where) {
23074
+ const wanted = [...new Set(sourceIds)].sort().join("\0");
23075
+ const target = normalizeForDedupe(content);
23076
+ const rows = await ctx.vaultMemoryCollection.query(
23077
+ import_watermelondb18.Q.where("trust_tier", "quarantined"),
23078
+ import_watermelondb18.Q.where("is_deleted", false),
23079
+ import_watermelondb18.Q.where("scope", where.scope),
23080
+ import_watermelondb18.Q.where("folder_id", where.folderId),
23081
+ ...ctx.userId !== void 0 ? [import_watermelondb18.Q.where("user_id", ctx.userId)] : []
23082
+ ).unsafeFetchRaw();
23083
+ for (const raw of rows) {
23084
+ let ids;
23085
+ try {
23086
+ ids = JSON.parse(raw.source_chunk_ids ?? "[]");
23087
+ } catch {
23088
+ continue;
23089
+ }
23090
+ if (!Array.isArray(ids)) continue;
23091
+ const key = [...new Set(ids.filter((id) => typeof id === "string"))].sort().join("\0");
23092
+ if (key !== wanted) continue;
23093
+ const stored = await vaultMemoryRawToStored(
23094
+ raw,
23095
+ ctx.walletAddress,
23096
+ ctx.signMessage,
23097
+ ctx.embeddedWalletSigner
23098
+ );
23099
+ if (normalizeForDedupe(stored.content) === target) return stored.uniqueId;
23100
+ }
23101
+ return null;
23102
+ }
23080
23103
 
23081
23104
  // src/lib/memory/decay.ts
23082
23105
  init_logger();
@@ -23101,6 +23124,10 @@ var DEFAULT_DECAY_POLICY = {
23101
23124
  hardDeleteWindowMs: HARD_DELETE_WINDOW_MS,
23102
23125
  pastEventGraceMs: PAST_EVENT_GRACE_MS
23103
23126
  };
23127
+ function lastActivityAt(m) {
23128
+ const observedAt = Number.isFinite(m.lastObservedAt) ? m.lastObservedAt : m.updatedAt;
23129
+ return Math.max(m.updatedAt, observedAt);
23130
+ }
23104
23131
  function resolvePolicy(policy) {
23105
23132
  if (!policy) return DEFAULT_DECAY_POLICY;
23106
23133
  return {
@@ -23140,8 +23167,7 @@ function classifyDecay(m, now, policy) {
23140
23167
  if (m.eventTimeEnd < now - resolved.pastEventGraceMs) return "archive";
23141
23168
  if (m.eventTimeEnd >= now) return "keep";
23142
23169
  }
23143
- const observedAt = Number.isFinite(m.lastObservedAt) ? m.lastObservedAt : m.updatedAt;
23144
- if (now - Math.max(m.updatedAt, observedAt) > ttlForType(m.factType, resolved)) return "archive";
23170
+ if (now - lastActivityAt(m) > ttlForType(m.factType, resolved)) return "archive";
23145
23171
  return "keep";
23146
23172
  }
23147
23173
 
@@ -28701,6 +28727,9 @@ var MANUAL_FACT_TYPES = [
28701
28727
  function normalizeManualFactType(value) {
28702
28728
  return typeof value === "string" && MANUAL_FACT_TYPES.includes(value) ? value : void 0;
28703
28729
  }
28730
+ function isSharedScope(scope) {
28731
+ return scope === "shared";
28732
+ }
28704
28733
  async function notifyWritten(options, input, outcome) {
28705
28734
  try {
28706
28735
  await options?.onWritten?.({ input, outcome });
@@ -28769,14 +28798,17 @@ function createMemoryVaultTool(vaultCtx, options, embeddingOptions, cache) {
28769
28798
  try {
28770
28799
  const isUpdate = !!id;
28771
28800
  let previousContent;
28801
+ const scope = options?.scope ?? "private";
28772
28802
  if (isUpdate) {
28773
28803
  const existing = await getVaultMemoryOp(vaultCtx, id);
28774
28804
  if (!existing) {
28775
28805
  return `Error: Memory with ID "${id}" not found. Creating a new memory instead would require a separate call without an ID.`;
28776
28806
  }
28807
+ if (isSharedScope(existing.scope) !== isSharedScope(scope)) {
28808
+ 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.`;
28809
+ }
28777
28810
  previousContent = existing.content;
28778
28811
  }
28779
- const scope = options?.scope ?? "private";
28780
28812
  const operation = {
28781
28813
  action: isUpdate ? "update" : "add",
28782
28814
  content,
@@ -29986,6 +30018,24 @@ async function extractAndRetain(messages, retainCtx, options) {
29986
30018
  const tRetain = Date.now();
29987
30019
  for (const { candidate, isQuarantined, reason, signature } of toRetain) {
29988
30020
  try {
30021
+ if (isQuarantined) {
30022
+ const existingId = await findQuarantinedDuplicateOp(
30023
+ retainCtx.vaultCtx,
30024
+ candidate.content,
30025
+ candidate.sourceMessageIds,
30026
+ // The same defaults retain() writes the row with.
30027
+ { scope: options.scope ?? "private", folderId: options.folderId ?? null }
30028
+ ).catch(() => null);
30029
+ if (existingId) {
30030
+ quarantinedInfo.push({
30031
+ candidate,
30032
+ memoryId: existingId,
30033
+ reason,
30034
+ signature
30035
+ });
30036
+ continue;
30037
+ }
30038
+ }
29989
30039
  const result = await retain(candidate.content, retainCtx, {
29990
30040
  source: "auto-extracted",
29991
30041
  sourceChunkIds: candidate.sourceMessageIds,
@@ -30436,7 +30486,7 @@ function createLlmDecayClassifier(options) {
30436
30486
  if (!content || content.trim().length === 0) return ruleVerdict;
30437
30487
  const trimmed = content.trim().slice(0, MAX_CONTENT_CHARS);
30438
30488
  const safe = redactor ? (await redactor.redactTextAsync(trimmed)).text : trimmed;
30439
- const meta = `factType: ${input.factType ?? "none"}; ageDays: ${ageDays(input.updatedAt, now)}`;
30489
+ const meta = `factType: ${input.factType ?? "none"}; ageDays: ${ageDays(lastActivityAt(input), now)}`;
30440
30490
  let parsed;
30441
30491
  try {
30442
30492
  parsed = await callPortalJsonCompletion({
@@ -30469,9 +30519,9 @@ Should this be kept or archived?`,
30469
30519
  }
30470
30520
  };
30471
30521
  }
30472
- function ageDays(updatedAt, now) {
30473
- if (!Number.isFinite(updatedAt) || !Number.isFinite(now)) return 0;
30474
- return Math.max(0, Math.floor((now - updatedAt) / (24 * 60 * 60 * 1e3)));
30522
+ function ageDays(activityAt, now) {
30523
+ if (!Number.isFinite(activityAt) || !Number.isFinite(now)) return 0;
30524
+ return Math.max(0, Math.floor((now - activityAt) / (24 * 60 * 60 * 1e3)));
30475
30525
  }
30476
30526
  function parseVerdict(parsed) {
30477
30527
  if (typeof parsed !== "object" || parsed === null) return null;
@@ -30525,7 +30575,7 @@ function createDecaySweeper(options) {
30525
30575
  if (!classifier || !isBorderline(input)) return ruleVerdict;
30526
30576
  if (input.id) {
30527
30577
  const cached = classifierCache.get(input.id);
30528
- if (cached && cached.updatedAt === input.updatedAt) return cached.verdict;
30578
+ if (cached && cached.activityAt === lastActivityAt(input)) return cached.verdict;
30529
30579
  }
30530
30580
  if (sweep.classifierCalls >= maxClassifierCalls) {
30531
30581
  if (!sweep.ceilingLogged) {
@@ -30539,7 +30589,7 @@ function createDecaySweeper(options) {
30539
30589
  sweep.classifierCalls++;
30540
30590
  try {
30541
30591
  const verdict = await classifier.classify(input, ruleVerdict, now);
30542
- if (input.id) classifierCache.set(input.id, { updatedAt: input.updatedAt, verdict });
30592
+ if (input.id) classifierCache.set(input.id, { activityAt: lastActivityAt(input), verdict });
30543
30593
  return verdict;
30544
30594
  } catch (err) {
30545
30595
  getLogger().warn(
@@ -31287,7 +31337,10 @@ function reviewedMemoryIdsSignature(ids) {
31287
31337
  return [...new Set(ids)].sort().join("\n");
31288
31338
  }
31289
31339
  function facetsSignature(facets) {
31290
- return facets.map((f) => JSON.stringify([f.key, f.label, f.query, f.guidance, facetResponseSchema(f.key)])).sort().join("\n");
31340
+ return [
31341
+ JSON.stringify(FACET_SYSTEM_PROMPT),
31342
+ ...facets.map((f) => JSON.stringify([f.key, f.label, f.query, f.guidance, facetResponseSchema(f.key)])).sort()
31343
+ ].join("\n");
31291
31344
  }
31292
31345
  function trendsEqual(a, b) {
31293
31346
  if (!a) return false;
@@ -31509,7 +31562,7 @@ var FACET_SYSTEM_PROMPT = `You are writing one section of a person's shareable p
31509
31562
  Rules:
31510
31563
  - Ground every claim in the supplied memories \u2014 never invent, infer beyond, or embellish what they support.
31511
31564
  - If the memories don't cover this section, return an empty summary with hasEvidence=false. Do not pad or guess.
31512
- - Write in third person about the person, in a natural voice suitable for a public profile (no "I"/"you", no name repetition).
31565
+ - 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.
31513
31566
  - Be concise and specific; no preamble, hedging, or meta-commentary.
31514
31567
  - Respond as JSON in exactly the shape the user turn states, with no extra fields.`;
31515
31568
  function buildFacetUserInstructions(facet) {
@@ -32184,6 +32237,37 @@ async function assembleMemoryContext(options) {
32184
32237
  var import_watermelondb24 = __toESM(require_watermelondb());
32185
32238
  var MAX_JOBS_PER_PASS = 3;
32186
32239
  var MAX_ATTEMPTS = 3;
32240
+ var MAX_FAILED_SESSIONS = 3;
32241
+ var MIN_FAILED_SESSION_GAP_MS = 60 * 60 * 1e3;
32242
+ var REQUEST_REJECTED_STATUSES = /* @__PURE__ */ new Set([400, 404, 413, 422]);
32243
+ var CONTENT_FAILURE_REASONS = /* @__PURE__ */ new Set([
32244
+ "empty-content",
32245
+ "invalid-json",
32246
+ "null-completion",
32247
+ "body-parse-failed"
32248
+ ]);
32249
+ var BACKOFF_ALLOWANCE_MS = 2100;
32250
+ var RETAIN_BUDGET_MS = 12e4;
32251
+ function defaultBatchTimeoutMs(extract) {
32252
+ const attempts = Math.max(1, extract.maxAttempts ?? 3);
32253
+ const calls = (extract.timeoutMs ?? 6e4) * attempts + BACKOFF_ALLOWANCE_MS * (attempts - 1);
32254
+ const extraction = extract.totalTimeoutMs !== void 0 ? Math.min(extract.totalTimeoutMs, calls) : calls;
32255
+ return extraction + RETAIN_BUDGET_MS;
32256
+ }
32257
+ var BatchFailureError = class extends Error {
32258
+ constructor(message, terminal) {
32259
+ super(message);
32260
+ this.terminal = terminal;
32261
+ }
32262
+ };
32263
+ var AccountFailureError = class extends Error {
32264
+ };
32265
+ var LockedSourcesError = class extends Error {
32266
+ constructor(persistentIds) {
32267
+ super("Source messages are locked; retained for retry");
32268
+ this.persistentIds = persistentIds;
32269
+ }
32270
+ };
32187
32271
  function idsOf(job) {
32188
32272
  return JSON.parse(String(job._getRaw("message_ids")));
32189
32273
  }
@@ -32191,6 +32275,16 @@ function seqOf(job) {
32191
32275
  const raw = job._getRaw("watermark_seq");
32192
32276
  return typeof raw === "number" && raw > 0 ? raw : void 0;
32193
32277
  }
32278
+ function failedSessionsOf(job, head) {
32279
+ if (job._getRaw("failed_head") !== head) return 0;
32280
+ const raw = job._getRaw("failed_sessions");
32281
+ return typeof raw === "number" && raw > 0 ? raw : 0;
32282
+ }
32283
+ function failedAtOf(job, head) {
32284
+ if (job._getRaw("failed_head") !== head) return void 0;
32285
+ const raw = job._getRaw("failed_at");
32286
+ return typeof raw === "number" ? raw : void 0;
32287
+ }
32194
32288
  function createDurableAutoExtractor(options) {
32195
32289
  const vault = options.retainCtx.vaultCtx;
32196
32290
  const database = vault.database;
@@ -32213,6 +32307,9 @@ function createDurableAutoExtractor(options) {
32213
32307
  let writes = Promise.resolve();
32214
32308
  let storeFailures = 0;
32215
32309
  const attempts = /* @__PURE__ */ new Map();
32310
+ const sessionFailures = /* @__PURE__ */ new Map();
32311
+ const skippedThisSession = /* @__PURE__ */ new Set();
32312
+ const clock = options.now ?? Date.now;
32216
32313
  const reportError = (error, conversationId) => {
32217
32314
  try {
32218
32315
  options.onError?.(error instanceof Error ? error : new Error(String(error)), conversationId);
@@ -32241,6 +32338,7 @@ function createDurableAutoExtractor(options) {
32241
32338
  }
32242
32339
  async function extractBatch(messages, conversationId, ids, jobScope, jobFolderId) {
32243
32340
  let watchdog;
32341
+ let cancelled = false;
32244
32342
  const model = options.modelForScope?.(jobScope) ?? options.extract.model;
32245
32343
  try {
32246
32344
  return await Promise.race([
@@ -32251,7 +32349,7 @@ function createDurableAutoExtractor(options) {
32251
32349
  ...options.retainCtx,
32252
32350
  vaultCtx: {
32253
32351
  ...vault,
32254
- 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
32352
+ 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
32255
32353
  }
32256
32354
  },
32257
32355
  // The model follows the job's scope, not the mode alive at drain time.
@@ -32271,14 +32369,116 @@ function createDurableAutoExtractor(options) {
32271
32369
  new Promise((_resolve, reject) => {
32272
32370
  watchdog = setTimeout(
32273
32371
  () => reject(new Error("Extraction did not settle in time; retained for retry")),
32274
- options.batchTimeoutMs ?? 18e4
32372
+ options.batchTimeoutMs ?? defaultBatchTimeoutMs(options.extract)
32275
32373
  );
32276
32374
  })
32277
32375
  ]);
32278
32376
  } finally {
32377
+ cancelled = true;
32279
32378
  if (watchdog !== void 0) clearTimeout(watchdog);
32280
32379
  }
32281
32380
  }
32381
+ async function acknowledge(job, ids, loaded, abandoned) {
32382
+ attempts.delete(job.id);
32383
+ sessionFailures.delete(job.id);
32384
+ skippedThisSession.delete(job.id);
32385
+ const sequences = /* @__PURE__ */ new Map();
32386
+ for (const message of loaded)
32387
+ if (message && message.messageId > 0) sequences.set(message.uniqueId, message.messageId);
32388
+ await database.write(async () => {
32389
+ const current = idsOf(job);
32390
+ const acknowledged = ids.filter((id) => current.includes(id));
32391
+ if (!acknowledged.length) return;
32392
+ const done = new Set(acknowledged);
32393
+ const remaining = current.filter((id) => !done.has(id));
32394
+ await job.update((r) => {
32395
+ r._setRaw(
32396
+ "message_ids",
32397
+ JSON.stringify(
32398
+ remaining.length ? [...abandoned ? [] : acknowledged.slice(-2), ...remaining] : []
32399
+ )
32400
+ );
32401
+ const newest = acknowledged[acknowledged.length - 1];
32402
+ r._setRaw("watermark", newest);
32403
+ const sequence = sequences.get(newest);
32404
+ if (sequence !== void 0) {
32405
+ const prior = seqOf(job);
32406
+ r._setRaw("watermark_seq", prior === void 0 ? sequence : Math.max(prior, sequence));
32407
+ }
32408
+ r._setRaw("failed_sessions", abandoned ? MAX_FAILED_SESSIONS : null);
32409
+ r._setRaw("failed_head", abandoned ? newest : null);
32410
+ r._setRaw("failed_at", null);
32411
+ });
32412
+ });
32413
+ }
32414
+ async function recordFailure(job, ids, loaded, error, conversationId) {
32415
+ const head = ids[0];
32416
+ if (!head || !loaded.some(Boolean)) return false;
32417
+ if (error instanceof AccountFailureError) {
32418
+ skippedThisSession.add(job.id);
32419
+ return false;
32420
+ }
32421
+ const counted = error instanceof BatchFailureError || error instanceof LockedSourcesError && error.persistentIds.length > 0;
32422
+ if (!counted) return false;
32423
+ const prior = sessionFailures.get(job.id);
32424
+ const count = prior?.head === head ? prior.count + 1 : 1;
32425
+ sessionFailures.set(job.id, { head, count });
32426
+ const terminal = error instanceof BatchFailureError && error.terminal;
32427
+ if (!terminal && count < MAX_ATTEMPTS) return false;
32428
+ skippedThisSession.add(job.id);
32429
+ const now = clock();
32430
+ const lastCounted = failedAtOf(job, head);
32431
+ if (lastCounted !== void 0 && now - lastCounted < MIN_FAILED_SESSION_GAP_MS) return false;
32432
+ const failures = failedSessionsOf(job, head) + 1;
32433
+ if (failures < MAX_FAILED_SESSIONS) {
32434
+ await database.write(
32435
+ () => job.update((r) => {
32436
+ r._setRaw("failed_sessions", failures);
32437
+ r._setRaw("failed_head", head);
32438
+ r._setRaw("failed_at", now);
32439
+ })
32440
+ );
32441
+ return false;
32442
+ }
32443
+ if (error instanceof LockedSourcesError) {
32444
+ const dropped = new Set(error.persistentIds);
32445
+ let newest;
32446
+ for (const message of loaded)
32447
+ if (message && dropped.has(message.uniqueId) && message.messageId > (newest?.seq ?? 0))
32448
+ newest = { id: message.uniqueId, seq: message.messageId };
32449
+ const advance = newest !== void 0 && newest.seq > (seqOf(job) ?? 0) ? newest : void 0;
32450
+ await database.write(
32451
+ () => job.update((r) => {
32452
+ r._setRaw("message_ids", JSON.stringify(idsOf(job).filter((id) => !dropped.has(id))));
32453
+ if (advance) {
32454
+ r._setRaw("watermark", advance.id);
32455
+ r._setRaw("watermark_seq", advance.seq);
32456
+ }
32457
+ r._setRaw("failed_sessions", advance ? MAX_FAILED_SESSIONS : null);
32458
+ r._setRaw("failed_head", advance ? advance.id : null);
32459
+ r._setRaw("failed_at", null);
32460
+ })
32461
+ );
32462
+ attempts.delete(job.id);
32463
+ sessionFailures.delete(job.id);
32464
+ skippedThisSession.delete(job.id);
32465
+ reportError(
32466
+ new Error(
32467
+ `Dropped ${dropped.size} source id(s) that stayed locked for ${failures} sessions to unblock extraction`
32468
+ ),
32469
+ conversationId
32470
+ );
32471
+ return true;
32472
+ }
32473
+ await acknowledge(job, ids, loaded, true);
32474
+ reportError(
32475
+ new Error(
32476
+ `Abandoned an extraction batch of ${ids.length} source id(s) after it failed in ${failures} sessions`
32477
+ ),
32478
+ conversationId
32479
+ );
32480
+ return true;
32481
+ }
32282
32482
  async function drain() {
32283
32483
  if (disposed || running) return;
32284
32484
  running = true;
@@ -32293,21 +32493,23 @@ function createDurableAutoExtractor(options) {
32293
32493
  for (const job of pending) {
32294
32494
  if (disposed) break;
32295
32495
  const attempted = attempts.get(job.id) ?? 0;
32296
- if (attempted >= MAX_ATTEMPTS) continue;
32496
+ if (attempted >= MAX_ATTEMPTS || skippedThisSession.has(job.id)) continue;
32297
32497
  if (processed >= MAX_JOBS_PER_PASS) {
32298
32498
  scheduleAfterSuccess = true;
32299
32499
  break;
32300
32500
  }
32301
32501
  processed++;
32302
32502
  const conversationId = String(job._getRaw("conversation_id"));
32503
+ let ids = [];
32504
+ let loaded = [];
32303
32505
  try {
32304
32506
  const conversations = await storage.conversationsCollection.query(import_watermelondb24.Q.where("conversation_id", conversationId), import_watermelondb24.Q.where("is_deleted", false)).fetchCount();
32305
32507
  if (!conversations) {
32306
32508
  await database.write(() => job.destroyPermanently());
32307
32509
  continue;
32308
32510
  }
32309
- const ids = idsOf(job).slice(0, 20);
32310
- const loaded = await Promise.all(ids.map((id) => getMessageOp(storage, id)));
32511
+ ids = idsOf(job).slice(0, 20);
32512
+ loaded = await Promise.all(ids.map((id) => getMessageOp(storage, id)));
32311
32513
  const missing = ids.filter((_id, index) => loaded[index] === null);
32312
32514
  if (missing.length) {
32313
32515
  if (attempted < MAX_ATTEMPTS - 1)
@@ -32331,17 +32533,21 @@ function createDurableAutoExtractor(options) {
32331
32533
  );
32332
32534
  continue;
32333
32535
  }
32334
- if (loaded.some(
32335
- (message) => message && (message.decryptionStatus || isEncrypted(message.content))
32336
- )) {
32337
- throw new Error("Source messages are locked; retained for retry");
32536
+ const locked = loaded.filter((message) => message && isEncrypted(message.content));
32537
+ if (locked.length) {
32538
+ throw new LockedSourcesError(
32539
+ locked.flatMap(
32540
+ (message) => message.decryptionStatus === "auth_mismatch" || message.decryptionStatus === "invalid_payload" ? [message.uniqueId] : []
32541
+ )
32542
+ );
32338
32543
  }
32339
32544
  const messages = loaded.flatMap(
32340
32545
  (m) => m && m.conversationId === conversationId && (m.role === "user" || m.role === "assistant") ? [{ id: m.uniqueId, role: m.role, content: m.content }] : []
32341
32546
  );
32342
32547
  let success = messages.length === 0;
32548
+ let result;
32343
32549
  if (messages.length) {
32344
- const result = await extractBatch(
32550
+ result = await extractBatch(
32345
32551
  messages,
32346
32552
  conversationId,
32347
32553
  ids,
@@ -32351,41 +32557,35 @@ function createDurableAutoExtractor(options) {
32351
32557
  success = result.failedCount === 0 && result.outcome !== "empty-after-retry";
32352
32558
  options.onTurnComplete?.(result);
32353
32559
  }
32354
- if (!success) throw new Error("Extraction batch incomplete; retained for retry");
32355
- attempts.delete(job.id);
32356
- const sequences = /* @__PURE__ */ new Map();
32357
- for (const message of loaded)
32358
- if (message && message.messageId > 0)
32359
- sequences.set(message.uniqueId, message.messageId);
32360
- await database.write(async () => {
32361
- const current = idsOf(job);
32362
- const acknowledged = ids.filter((id) => current.includes(id));
32363
- if (!acknowledged.length) return;
32364
- const done = new Set(acknowledged);
32365
- const remaining = current.filter((id) => !done.has(id));
32366
- if (remaining.length) {
32367
- await job.update(
32368
- (r) => r._setRaw("message_ids", JSON.stringify([...acknowledged.slice(-2), ...remaining]))
32369
- );
32370
- } else await job.update((r) => r._setRaw("message_ids", "[]"));
32371
- const newest = acknowledged[acknowledged.length - 1];
32372
- await job.update((r) => {
32373
- r._setRaw("watermark", newest);
32374
- const sequence = sequences.get(newest);
32375
- if (sequence !== void 0) {
32376
- const prior = seqOf(job);
32377
- r._setRaw(
32378
- "watermark_seq",
32379
- prior === void 0 ? sequence : Math.max(prior, sequence)
32560
+ if (!success) {
32561
+ const failure = result?.failure;
32562
+ if (failure?.reason === "http-terminal") {
32563
+ const status = failure.httpStatus;
32564
+ if (status !== void 0 && REQUEST_REJECTED_STATUSES.has(status))
32565
+ throw new BatchFailureError(
32566
+ `Extraction rejected the batch (HTTP ${status}); not retried this session`,
32567
+ true
32380
32568
  );
32381
- }
32382
- });
32383
- });
32569
+ throw new AccountFailureError(
32570
+ `Extraction refused for the account (HTTP ${status ?? "unknown"}); retried next session`
32571
+ );
32572
+ }
32573
+ if ((result?.failedCount ?? 0) > 0 || failure !== void 0 && CONTENT_FAILURE_REASONS.has(failure.reason))
32574
+ throw new BatchFailureError("Extraction batch incomplete; retained for retry", false);
32575
+ throw new Error("Extraction batch incomplete; retained for retry");
32576
+ }
32577
+ await acknowledge(job, ids, loaded, false);
32384
32578
  scheduleAfterSuccess = true;
32385
32579
  } catch (error) {
32386
32580
  attempts.set(job.id, attempted + 1);
32387
32581
  retry = true;
32388
32582
  reportError(error, conversationId);
32583
+ try {
32584
+ if (await recordFailure(job, ids, loaded, error, conversationId))
32585
+ scheduleAfterSuccess = true;
32586
+ } catch (recordError) {
32587
+ reportError(recordError, conversationId);
32588
+ }
32389
32589
  }
32390
32590
  }
32391
32591
  } catch (error) {
@@ -32455,7 +32655,7 @@ function createDurableAutoExtractor(options) {
32455
32655
  const start = boundary >= 0 ? boundary + 1 : Math.max(0, ids.length - Math.max(1, options.windowSize ?? 6));
32456
32656
  if (start >= ids.length) return;
32457
32657
  const ownWatermark = job?._getRaw("watermark");
32458
- const atOwnWatermark = !!job && boundary >= 0 && (ids[boundary] === ownWatermark || sequences.get(ids[boundary]) !== void 0 && sequences.get(ids[boundary]) === seqOf(job));
32658
+ 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));
32459
32659
  const overlapStart = atOwnWatermark ? Math.max(0, boundary - 1) : start;
32460
32660
  const fresh = ids.slice(overlapStart).filter((id) => !foreignSources.has(id));
32461
32661
  if (!fresh.length) return;