@anuma/sdk 6.25.1 → 6.25.2

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(
@@ -32184,6 +32234,37 @@ async function assembleMemoryContext(options) {
32184
32234
  var import_watermelondb24 = __toESM(require_watermelondb());
32185
32235
  var MAX_JOBS_PER_PASS = 3;
32186
32236
  var MAX_ATTEMPTS = 3;
32237
+ var MAX_FAILED_SESSIONS = 3;
32238
+ var MIN_FAILED_SESSION_GAP_MS = 60 * 60 * 1e3;
32239
+ var REQUEST_REJECTED_STATUSES = /* @__PURE__ */ new Set([400, 404, 413, 422]);
32240
+ var CONTENT_FAILURE_REASONS = /* @__PURE__ */ new Set([
32241
+ "empty-content",
32242
+ "invalid-json",
32243
+ "null-completion",
32244
+ "body-parse-failed"
32245
+ ]);
32246
+ var BACKOFF_ALLOWANCE_MS = 2100;
32247
+ var RETAIN_BUDGET_MS = 12e4;
32248
+ function defaultBatchTimeoutMs(extract) {
32249
+ const attempts = Math.max(1, extract.maxAttempts ?? 3);
32250
+ const calls = (extract.timeoutMs ?? 6e4) * attempts + BACKOFF_ALLOWANCE_MS * (attempts - 1);
32251
+ const extraction = extract.totalTimeoutMs !== void 0 ? Math.min(extract.totalTimeoutMs, calls) : calls;
32252
+ return extraction + RETAIN_BUDGET_MS;
32253
+ }
32254
+ var BatchFailureError = class extends Error {
32255
+ constructor(message, terminal) {
32256
+ super(message);
32257
+ this.terminal = terminal;
32258
+ }
32259
+ };
32260
+ var AccountFailureError = class extends Error {
32261
+ };
32262
+ var LockedSourcesError = class extends Error {
32263
+ constructor(persistentIds) {
32264
+ super("Source messages are locked; retained for retry");
32265
+ this.persistentIds = persistentIds;
32266
+ }
32267
+ };
32187
32268
  function idsOf(job) {
32188
32269
  return JSON.parse(String(job._getRaw("message_ids")));
32189
32270
  }
@@ -32191,6 +32272,16 @@ function seqOf(job) {
32191
32272
  const raw = job._getRaw("watermark_seq");
32192
32273
  return typeof raw === "number" && raw > 0 ? raw : void 0;
32193
32274
  }
32275
+ function failedSessionsOf(job, head) {
32276
+ if (job._getRaw("failed_head") !== head) return 0;
32277
+ const raw = job._getRaw("failed_sessions");
32278
+ return typeof raw === "number" && raw > 0 ? raw : 0;
32279
+ }
32280
+ function failedAtOf(job, head) {
32281
+ if (job._getRaw("failed_head") !== head) return void 0;
32282
+ const raw = job._getRaw("failed_at");
32283
+ return typeof raw === "number" ? raw : void 0;
32284
+ }
32194
32285
  function createDurableAutoExtractor(options) {
32195
32286
  const vault = options.retainCtx.vaultCtx;
32196
32287
  const database = vault.database;
@@ -32213,6 +32304,9 @@ function createDurableAutoExtractor(options) {
32213
32304
  let writes = Promise.resolve();
32214
32305
  let storeFailures = 0;
32215
32306
  const attempts = /* @__PURE__ */ new Map();
32307
+ const sessionFailures = /* @__PURE__ */ new Map();
32308
+ const skippedThisSession = /* @__PURE__ */ new Set();
32309
+ const clock = options.now ?? Date.now;
32216
32310
  const reportError = (error, conversationId) => {
32217
32311
  try {
32218
32312
  options.onError?.(error instanceof Error ? error : new Error(String(error)), conversationId);
@@ -32241,6 +32335,7 @@ function createDurableAutoExtractor(options) {
32241
32335
  }
32242
32336
  async function extractBatch(messages, conversationId, ids, jobScope, jobFolderId) {
32243
32337
  let watchdog;
32338
+ let cancelled = false;
32244
32339
  const model = options.modelForScope?.(jobScope) ?? options.extract.model;
32245
32340
  try {
32246
32341
  return await Promise.race([
@@ -32251,7 +32346,7 @@ function createDurableAutoExtractor(options) {
32251
32346
  ...options.retainCtx,
32252
32347
  vaultCtx: {
32253
32348
  ...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
32349
+ 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
32350
  }
32256
32351
  },
32257
32352
  // The model follows the job's scope, not the mode alive at drain time.
@@ -32271,14 +32366,116 @@ function createDurableAutoExtractor(options) {
32271
32366
  new Promise((_resolve, reject) => {
32272
32367
  watchdog = setTimeout(
32273
32368
  () => reject(new Error("Extraction did not settle in time; retained for retry")),
32274
- options.batchTimeoutMs ?? 18e4
32369
+ options.batchTimeoutMs ?? defaultBatchTimeoutMs(options.extract)
32275
32370
  );
32276
32371
  })
32277
32372
  ]);
32278
32373
  } finally {
32374
+ cancelled = true;
32279
32375
  if (watchdog !== void 0) clearTimeout(watchdog);
32280
32376
  }
32281
32377
  }
32378
+ async function acknowledge(job, ids, loaded, abandoned) {
32379
+ attempts.delete(job.id);
32380
+ sessionFailures.delete(job.id);
32381
+ skippedThisSession.delete(job.id);
32382
+ const sequences = /* @__PURE__ */ new Map();
32383
+ for (const message of loaded)
32384
+ if (message && message.messageId > 0) sequences.set(message.uniqueId, message.messageId);
32385
+ await database.write(async () => {
32386
+ const current = idsOf(job);
32387
+ const acknowledged = ids.filter((id) => current.includes(id));
32388
+ if (!acknowledged.length) return;
32389
+ const done = new Set(acknowledged);
32390
+ const remaining = current.filter((id) => !done.has(id));
32391
+ await job.update((r) => {
32392
+ r._setRaw(
32393
+ "message_ids",
32394
+ JSON.stringify(
32395
+ remaining.length ? [...abandoned ? [] : acknowledged.slice(-2), ...remaining] : []
32396
+ )
32397
+ );
32398
+ const newest = acknowledged[acknowledged.length - 1];
32399
+ r._setRaw("watermark", newest);
32400
+ const sequence = sequences.get(newest);
32401
+ if (sequence !== void 0) {
32402
+ const prior = seqOf(job);
32403
+ r._setRaw("watermark_seq", prior === void 0 ? sequence : Math.max(prior, sequence));
32404
+ }
32405
+ r._setRaw("failed_sessions", abandoned ? MAX_FAILED_SESSIONS : null);
32406
+ r._setRaw("failed_head", abandoned ? newest : null);
32407
+ r._setRaw("failed_at", null);
32408
+ });
32409
+ });
32410
+ }
32411
+ async function recordFailure(job, ids, loaded, error, conversationId) {
32412
+ const head = ids[0];
32413
+ if (!head || !loaded.some(Boolean)) return false;
32414
+ if (error instanceof AccountFailureError) {
32415
+ skippedThisSession.add(job.id);
32416
+ return false;
32417
+ }
32418
+ const counted = error instanceof BatchFailureError || error instanceof LockedSourcesError && error.persistentIds.length > 0;
32419
+ if (!counted) return false;
32420
+ const prior = sessionFailures.get(job.id);
32421
+ const count = prior?.head === head ? prior.count + 1 : 1;
32422
+ sessionFailures.set(job.id, { head, count });
32423
+ const terminal = error instanceof BatchFailureError && error.terminal;
32424
+ if (!terminal && count < MAX_ATTEMPTS) return false;
32425
+ skippedThisSession.add(job.id);
32426
+ const now = clock();
32427
+ const lastCounted = failedAtOf(job, head);
32428
+ if (lastCounted !== void 0 && now - lastCounted < MIN_FAILED_SESSION_GAP_MS) return false;
32429
+ const failures = failedSessionsOf(job, head) + 1;
32430
+ if (failures < MAX_FAILED_SESSIONS) {
32431
+ await database.write(
32432
+ () => job.update((r) => {
32433
+ r._setRaw("failed_sessions", failures);
32434
+ r._setRaw("failed_head", head);
32435
+ r._setRaw("failed_at", now);
32436
+ })
32437
+ );
32438
+ return false;
32439
+ }
32440
+ if (error instanceof LockedSourcesError) {
32441
+ const dropped = new Set(error.persistentIds);
32442
+ let newest;
32443
+ for (const message of loaded)
32444
+ if (message && dropped.has(message.uniqueId) && message.messageId > (newest?.seq ?? 0))
32445
+ newest = { id: message.uniqueId, seq: message.messageId };
32446
+ const advance = newest !== void 0 && newest.seq > (seqOf(job) ?? 0) ? newest : void 0;
32447
+ await database.write(
32448
+ () => job.update((r) => {
32449
+ r._setRaw("message_ids", JSON.stringify(idsOf(job).filter((id) => !dropped.has(id))));
32450
+ if (advance) {
32451
+ r._setRaw("watermark", advance.id);
32452
+ r._setRaw("watermark_seq", advance.seq);
32453
+ }
32454
+ r._setRaw("failed_sessions", advance ? MAX_FAILED_SESSIONS : null);
32455
+ r._setRaw("failed_head", advance ? advance.id : null);
32456
+ r._setRaw("failed_at", null);
32457
+ })
32458
+ );
32459
+ attempts.delete(job.id);
32460
+ sessionFailures.delete(job.id);
32461
+ skippedThisSession.delete(job.id);
32462
+ reportError(
32463
+ new Error(
32464
+ `Dropped ${dropped.size} source id(s) that stayed locked for ${failures} sessions to unblock extraction`
32465
+ ),
32466
+ conversationId
32467
+ );
32468
+ return true;
32469
+ }
32470
+ await acknowledge(job, ids, loaded, true);
32471
+ reportError(
32472
+ new Error(
32473
+ `Abandoned an extraction batch of ${ids.length} source id(s) after it failed in ${failures} sessions`
32474
+ ),
32475
+ conversationId
32476
+ );
32477
+ return true;
32478
+ }
32282
32479
  async function drain() {
32283
32480
  if (disposed || running) return;
32284
32481
  running = true;
@@ -32293,21 +32490,23 @@ function createDurableAutoExtractor(options) {
32293
32490
  for (const job of pending) {
32294
32491
  if (disposed) break;
32295
32492
  const attempted = attempts.get(job.id) ?? 0;
32296
- if (attempted >= MAX_ATTEMPTS) continue;
32493
+ if (attempted >= MAX_ATTEMPTS || skippedThisSession.has(job.id)) continue;
32297
32494
  if (processed >= MAX_JOBS_PER_PASS) {
32298
32495
  scheduleAfterSuccess = true;
32299
32496
  break;
32300
32497
  }
32301
32498
  processed++;
32302
32499
  const conversationId = String(job._getRaw("conversation_id"));
32500
+ let ids = [];
32501
+ let loaded = [];
32303
32502
  try {
32304
32503
  const conversations = await storage.conversationsCollection.query(import_watermelondb24.Q.where("conversation_id", conversationId), import_watermelondb24.Q.where("is_deleted", false)).fetchCount();
32305
32504
  if (!conversations) {
32306
32505
  await database.write(() => job.destroyPermanently());
32307
32506
  continue;
32308
32507
  }
32309
- const ids = idsOf(job).slice(0, 20);
32310
- const loaded = await Promise.all(ids.map((id) => getMessageOp(storage, id)));
32508
+ ids = idsOf(job).slice(0, 20);
32509
+ loaded = await Promise.all(ids.map((id) => getMessageOp(storage, id)));
32311
32510
  const missing = ids.filter((_id, index) => loaded[index] === null);
32312
32511
  if (missing.length) {
32313
32512
  if (attempted < MAX_ATTEMPTS - 1)
@@ -32331,17 +32530,21 @@ function createDurableAutoExtractor(options) {
32331
32530
  );
32332
32531
  continue;
32333
32532
  }
32334
- if (loaded.some(
32335
- (message) => message && (message.decryptionStatus || isEncrypted(message.content))
32336
- )) {
32337
- throw new Error("Source messages are locked; retained for retry");
32533
+ const locked = loaded.filter((message) => message && isEncrypted(message.content));
32534
+ if (locked.length) {
32535
+ throw new LockedSourcesError(
32536
+ locked.flatMap(
32537
+ (message) => message.decryptionStatus === "auth_mismatch" || message.decryptionStatus === "invalid_payload" ? [message.uniqueId] : []
32538
+ )
32539
+ );
32338
32540
  }
32339
32541
  const messages = loaded.flatMap(
32340
32542
  (m) => m && m.conversationId === conversationId && (m.role === "user" || m.role === "assistant") ? [{ id: m.uniqueId, role: m.role, content: m.content }] : []
32341
32543
  );
32342
32544
  let success = messages.length === 0;
32545
+ let result;
32343
32546
  if (messages.length) {
32344
- const result = await extractBatch(
32547
+ result = await extractBatch(
32345
32548
  messages,
32346
32549
  conversationId,
32347
32550
  ids,
@@ -32351,41 +32554,35 @@ function createDurableAutoExtractor(options) {
32351
32554
  success = result.failedCount === 0 && result.outcome !== "empty-after-retry";
32352
32555
  options.onTurnComplete?.(result);
32353
32556
  }
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)
32557
+ if (!success) {
32558
+ const failure = result?.failure;
32559
+ if (failure?.reason === "http-terminal") {
32560
+ const status = failure.httpStatus;
32561
+ if (status !== void 0 && REQUEST_REJECTED_STATUSES.has(status))
32562
+ throw new BatchFailureError(
32563
+ `Extraction rejected the batch (HTTP ${status}); not retried this session`,
32564
+ true
32380
32565
  );
32381
- }
32382
- });
32383
- });
32566
+ throw new AccountFailureError(
32567
+ `Extraction refused for the account (HTTP ${status ?? "unknown"}); retried next session`
32568
+ );
32569
+ }
32570
+ if ((result?.failedCount ?? 0) > 0 || failure !== void 0 && CONTENT_FAILURE_REASONS.has(failure.reason))
32571
+ throw new BatchFailureError("Extraction batch incomplete; retained for retry", false);
32572
+ throw new Error("Extraction batch incomplete; retained for retry");
32573
+ }
32574
+ await acknowledge(job, ids, loaded, false);
32384
32575
  scheduleAfterSuccess = true;
32385
32576
  } catch (error) {
32386
32577
  attempts.set(job.id, attempted + 1);
32387
32578
  retry = true;
32388
32579
  reportError(error, conversationId);
32580
+ try {
32581
+ if (await recordFailure(job, ids, loaded, error, conversationId))
32582
+ scheduleAfterSuccess = true;
32583
+ } catch (recordError) {
32584
+ reportError(recordError, conversationId);
32585
+ }
32389
32586
  }
32390
32587
  }
32391
32588
  } catch (error) {
@@ -32455,7 +32652,7 @@ function createDurableAutoExtractor(options) {
32455
32652
  const start = boundary >= 0 ? boundary + 1 : Math.max(0, ids.length - Math.max(1, options.windowSize ?? 6));
32456
32653
  if (start >= ids.length) return;
32457
32654
  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));
32655
+ 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
32656
  const overlapStart = atOwnWatermark ? Math.max(0, boundary - 1) : start;
32460
32657
  const fresh = ids.slice(overlapStart).filter((id) => !foreignSources.has(id));
32461
32658
  if (!fresh.length) return;