memorix 1.2.7 → 1.2.8

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.
@@ -25,6 +25,12 @@ type MaintenanceJobRunResult = {
25
25
  delayMs: number;
26
26
  resetAttempts?: boolean;
27
27
  payload?: Record<string, unknown>;
28
+ /** Keep recoverable work out of the immediate pending queue during a cooldown. */
29
+ status?: 'pending' | 'retry';
30
+ /** Persist a sanitized diagnostic without consuming the job's retry budget. */
31
+ lastError?: string;
32
+ /** Clear a stale transient diagnostic after the job makes progress. */
33
+ clearLastError?: boolean;
28
34
  };
29
35
 
30
36
  interface IsolatedMaintenanceRequest {
@@ -5918,6 +5918,10 @@ var init_maintenance_jobs = __esm({
5918
5918
  LIMIT 1
5919
5919
  `).get(input.projectId, input.kind, dedupeKey);
5920
5920
  if (existing) {
5921
+ if (input.kind === "vector-backfill" && existing.status === "retry") {
5922
+ this.commit();
5923
+ return rowToJob(existing);
5924
+ }
5921
5925
  const nextRunAfter = Math.min(Number(existing.run_after), runAfter);
5922
5926
  this.db.prepare(`
5923
5927
  UPDATE maintenance_jobs
@@ -5928,6 +5932,26 @@ var init_maintenance_jobs = __esm({
5928
5932
  this.commit();
5929
5933
  return rowToJob(updated);
5930
5934
  }
5935
+ if (input.kind === "vector-backfill") {
5936
+ const failed = this.db.prepare(`
5937
+ SELECT * FROM maintenance_jobs
5938
+ WHERE project_id = ? AND kind = ? AND dedupe_key = ? AND status = 'failed'
5939
+ ORDER BY updated_at DESC
5940
+ LIMIT 1
5941
+ `).get(input.projectId, input.kind, dedupeKey);
5942
+ if (failed) {
5943
+ this.db.prepare(`
5944
+ UPDATE maintenance_jobs
5945
+ SET status = 'pending', attempts = 0, max_attempts = ?, run_after = ?,
5946
+ payload_json = ?, lease_owner = NULL, lease_expires_at = NULL,
5947
+ last_error = NULL, completed_at = NULL, updated_at = ?
5948
+ WHERE id = ?
5949
+ `).run(maxAttempts, runAfter, payloadJson, now3, failed.id);
5950
+ const revived = this.getRow(failed.id);
5951
+ this.commit();
5952
+ return rowToJob(revived);
5953
+ }
5954
+ }
5931
5955
  const id = randomUUID2();
5932
5956
  this.db.prepare(`
5933
5957
  INSERT INTO maintenance_jobs (
@@ -6099,15 +6123,42 @@ var init_maintenance_jobs = __esm({
6099
6123
  const now3 = options.now ?? Date.now();
6100
6124
  const delayMs = Number.isFinite(options.delayMs) ? Math.max(0, Math.floor(options.delayMs)) : 0;
6101
6125
  const payloadJson = options.payload === void 0 ? null : JSON.stringify(options.payload);
6126
+ const status = options.status === "retry" ? "retry" : "pending";
6127
+ const hasLastError = options.lastError !== void 0;
6128
+ const lastError = hasLastError ? errorText(options.lastError) : null;
6102
6129
  this.db.prepare(`
6103
6130
  UPDATE maintenance_jobs
6104
- SET status = 'pending', run_after = ?, attempts = CASE WHEN ? THEN 0 ELSE attempts END,
6131
+ SET status = ?, run_after = ?, attempts = CASE WHEN ? THEN 0 ELSE attempts END,
6105
6132
  payload_json = COALESCE(?, payload_json),
6133
+ last_error = CASE
6134
+ WHEN ? THEN NULL
6135
+ WHEN ? THEN ?
6136
+ ELSE last_error
6137
+ END,
6106
6138
  lease_owner = NULL, lease_expires_at = NULL, updated_at = ?
6107
6139
  WHERE id = ? AND status = 'running' AND lease_owner = ?
6108
- `).run(now3 + delayMs, options.resetAttempts ? 1 : 0, payloadJson, now3, id, workerId);
6140
+ `).run(
6141
+ status,
6142
+ now3 + delayMs,
6143
+ options.resetAttempts ? 1 : 0,
6144
+ payloadJson,
6145
+ options.clearLastError ? 1 : 0,
6146
+ hasLastError ? 1 : 0,
6147
+ lastError,
6148
+ now3,
6149
+ id,
6150
+ workerId
6151
+ );
6109
6152
  return this.get(id);
6110
6153
  }
6154
+ resolveFailedVectorBackfills(projectId, dedupeKey = "vector-backfill", now3 = Date.now()) {
6155
+ const result = this.db.prepare(`
6156
+ UPDATE maintenance_jobs
6157
+ SET status = 'completed', completed_at = ?, updated_at = ?
6158
+ WHERE project_id = ? AND kind = 'vector-backfill' AND dedupe_key = ? AND status = 'failed'
6159
+ `).run(now3, now3, projectId, dedupeKey);
6160
+ return Number(result.changes ?? 0);
6161
+ }
6111
6162
  fail(id, workerId, error, now3 = Date.now()) {
6112
6163
  this.begin();
6113
6164
  try {
@@ -6203,6 +6254,9 @@ var init_maintenance_jobs = __esm({
6203
6254
  delayMs: result.delayMs,
6204
6255
  resetAttempts: result.resetAttempts,
6205
6256
  payload: result.payload,
6257
+ status: result.status,
6258
+ lastError: result.lastError,
6259
+ clearLastError: result.clearLastError,
6206
6260
  now: now3
6207
6261
  });
6208
6262
  return { state: "rescheduled", job: updated2 };
@@ -7162,6 +7216,9 @@ function normalizeEmbeddingFailure(error) {
7162
7216
  message: raw
7163
7217
  };
7164
7218
  }
7219
+ function vectorBackfillError(error) {
7220
+ return sanitizeCredentials(normalizeEmbeddingFailure(error).message).slice(0, 1e3);
7221
+ }
7165
7222
  function queueVectorBackfill(projectId) {
7166
7223
  const dataDir = projectDir;
7167
7224
  if (!dataDir) return;
@@ -7917,17 +7974,53 @@ async function backfillVectorEmbeddings(options = {}) {
7917
7974
  let succeeded = 0;
7918
7975
  let failed = 0;
7919
7976
  let lastFailure;
7977
+ const recordFailure = (message) => {
7978
+ if (!lastFailure) lastFailure = message;
7979
+ };
7980
+ const candidates = [];
7981
+ for (const id of ids) {
7982
+ const observation = observationById.get(id);
7983
+ if (!observation) {
7984
+ vectorMissingIds.delete(id);
7985
+ continue;
7986
+ }
7987
+ candidates.push({
7988
+ id,
7989
+ observation,
7990
+ text: [observation.title, observation.narrative, ...observation.facts].join(" ")
7991
+ });
7992
+ }
7920
7993
  try {
7921
- for (const id of ids) {
7922
- const obs = observationById.get(id);
7923
- if (!obs) {
7924
- vectorMissingIds.delete(id);
7925
- continue;
7994
+ if (candidates.length === 0) {
7995
+ return { attempted: ids.length, succeeded, failed };
7996
+ }
7997
+ let embeddings = null;
7998
+ try {
7999
+ const provider2 = await getEmbeddingProvider();
8000
+ if (!provider2) {
8001
+ if (isEmbeddingExplicitlyDisabled()) {
8002
+ for (const candidate of candidates) vectorMissingIds.delete(candidate.id);
8003
+ } else {
8004
+ recordFailure("embedding provider unavailable");
8005
+ failed += candidates.length;
8006
+ }
8007
+ } else {
8008
+ embeddings = await provider2.embedBatch(candidates.map((candidate) => candidate.text));
7926
8009
  }
7927
- const text = [obs.title, obs.narrative, ...obs.facts].join(" ");
7928
- try {
7929
- const embedding = await generateEmbedding(text);
7930
- if (embedding) {
8010
+ } catch (error) {
8011
+ recordFailure(vectorBackfillError(error));
8012
+ failed += candidates.length;
8013
+ }
8014
+ if (embeddings) {
8015
+ for (let index = 0; index < candidates.length; index++) {
8016
+ const { id, observation: obs } = candidates[index];
8017
+ const embedding = embeddings[index];
8018
+ if (!embedding || embedding.length === 0) {
8019
+ recordFailure("embedding provider returned no vector");
8020
+ failed++;
8021
+ continue;
8022
+ }
8023
+ try {
7931
8024
  if (!isVectorCompatibleWithCurrentIndex(embedding)) {
7932
8025
  await upgradeVectorSchemaAfterFirstEmbedding(embedding);
7933
8026
  }
@@ -7936,7 +8029,7 @@ async function backfillVectorEmbeddings(options = {}) {
7936
8029
  console.error(
7937
8030
  `[memorix] Backfill embedding mismatch for obs-${id}: provider returned ${embedding.length}d, index expects ${vectorDimensions ?? "unknown"}d (kept in queue)`
7938
8031
  );
7939
- lastFailure = `dimension mismatch: provider returned ${embedding.length}d, index expects ${vectorDimensions ?? "unknown"}d`;
8032
+ recordFailure(`dimension mismatch: provider returned ${embedding.length}d, index expects ${vectorDimensions ?? "unknown"}d`);
7940
8033
  failed++;
7941
8034
  continue;
7942
8035
  }
@@ -7975,15 +8068,10 @@ async function backfillVectorEmbeddings(options = {}) {
7975
8068
  await insertObservation(doc);
7976
8069
  vectorMissingIds.delete(id);
7977
8070
  succeeded++;
7978
- } else if (isEmbeddingExplicitlyDisabled()) {
7979
- vectorMissingIds.delete(id);
7980
- } else {
7981
- lastFailure = "embedding provider unavailable";
8071
+ } catch (error) {
8072
+ recordFailure(vectorBackfillError(error));
7982
8073
  failed++;
7983
8074
  }
7984
- } catch (err) {
7985
- lastFailure = err instanceof Error ? err.message : String(err);
7986
- failed++;
7987
8075
  }
7988
8076
  }
7989
8077
  } finally {
@@ -7996,7 +8084,12 @@ async function backfillVectorEmbeddings(options = {}) {
7996
8084
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
7997
8085
  };
7998
8086
  }
7999
- return { attempted: ids.length, succeeded, failed };
8087
+ return {
8088
+ attempted: ids.length,
8089
+ succeeded,
8090
+ failed,
8091
+ ...lastFailure ? { lastError: lastFailure } : {}
8092
+ };
8000
8093
  }
8001
8094
  var observations, nextId, projectDir, searchIndexPrepared, vectorMissingIds, vectorBackfillRunning, vectorSchemaUpgradePromise, lastVectorBackfill, embeddingFailureLogTimestamps, EMBEDDING_FAILURE_LOG_COOLDOWN_MS;
8002
8095
  var init_observations = __esm({
@@ -11915,6 +12008,7 @@ var DEFAULT_TIMEOUT_MS = 5 * 6e4;
11915
12008
 
11916
12009
  // src/runtime/project-maintenance.ts
11917
12010
  init_esm_shims();
12011
+ init_maintenance_jobs();
11918
12012
  init_lifecycle();
11919
12013
  init_timeout();
11920
12014
  var DEFAULT_VECTOR_BATCH_SIZE = 12;
@@ -11924,12 +12018,34 @@ var DEFAULT_CODEGRAPH_MAX_FILES = 5e3;
11924
12018
  var DEFAULT_CLAIM_DERIVATION_BATCH_SIZE = 100;
11925
12019
  var DEFAULT_OBSERVATION_QUALIFICATION_BATCH_SIZE = 100;
11926
12020
  var VECTOR_RETRY_DELAY_MS = 5e3;
12021
+ var VECTOR_FAILURE_BASE_RETRY_DELAY_MS = 6e4;
12022
+ var VECTOR_FAILURE_MAX_RETRY_DELAY_MS = 15 * 6e4;
11927
12023
  var DEDUP_PER_PAIR_TIMEOUT_MS = 5e3;
11928
12024
  function vectorBatchSize(payload) {
11929
12025
  const value = payload.limit;
11930
12026
  if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_VECTOR_BATCH_SIZE;
11931
12027
  return Math.min(100, Math.max(1, Math.floor(value)));
11932
12028
  }
12029
+ function vectorBackfillFailureStreak(payload) {
12030
+ const value = payload.vectorBackfillFailureStreak;
12031
+ return typeof value === "number" && Number.isFinite(value) ? Math.min(16, Math.max(0, Math.floor(value))) : 0;
12032
+ }
12033
+ function vectorBackfillRetryDelayMs(streak) {
12034
+ return Math.min(
12035
+ VECTOR_FAILURE_MAX_RETRY_DELAY_MS,
12036
+ VECTOR_FAILURE_BASE_RETRY_DELAY_MS * 2 ** Math.max(0, streak - 1)
12037
+ );
12038
+ }
12039
+ function withoutVectorBackfillFailureStreak(payload) {
12040
+ const { vectorBackfillFailureStreak: _streak, ...rest } = payload;
12041
+ return rest;
12042
+ }
12043
+ function resolveSupersededVectorFailures(projectDir2, projectId) {
12044
+ try {
12045
+ new MaintenanceJobStore(projectDir2).resolveFailedVectorBackfills(projectId);
12046
+ } catch {
12047
+ }
12048
+ }
11933
12049
  function retentionBatchSize(payload) {
11934
12050
  const value = payload.limit;
11935
12051
  if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_RETENTION_BATCH_SIZE;
@@ -12338,20 +12454,45 @@ function createProjectMaintenanceHandler(projectId, projectDir2, projectRoot, op
12338
12454
  if (isEmbeddingExplicitlyDisabled2()) return { action: "complete" };
12339
12455
  const { backfillVectorEmbeddings: backfillVectorEmbeddings2, getVectorStatus: getVectorStatus2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
12340
12456
  const before = getVectorStatus2(projectId);
12341
- if (before.missing === 0) return { action: "complete" };
12457
+ if (before.missing === 0) {
12458
+ resolveSupersededVectorFailures(projectDir2, projectId);
12459
+ return { action: "complete" };
12460
+ }
12342
12461
  const result = await backfillVectorEmbeddings2({
12343
12462
  projectId,
12344
12463
  limit: vectorBatchSize(job.payload)
12345
12464
  });
12346
12465
  const after = getVectorStatus2(projectId);
12347
- if (after.missing === 0) return { action: "complete" };
12466
+ if (after.missing === 0) {
12467
+ resolveSupersededVectorFailures(projectDir2, projectId);
12468
+ return { action: "complete" };
12469
+ }
12348
12470
  if (result.failed > 0 && result.succeeded === 0) {
12349
- throw new Error(`vector backfill made no progress (${result.failed}/${result.attempted} failed)`);
12471
+ const streak = vectorBackfillFailureStreak(job.payload) + 1;
12472
+ return {
12473
+ action: "reschedule",
12474
+ status: "retry",
12475
+ delayMs: vectorBackfillRetryDelayMs(streak),
12476
+ // This is a recoverable provider/index state, not a terminal job error.
12477
+ // Keeping one retry row prevents new MCP processes from creating a storm.
12478
+ resetAttempts: true,
12479
+ payload: {
12480
+ ...job.payload,
12481
+ vectorBackfillFailureStreak: streak
12482
+ },
12483
+ lastError: result.lastError ?? `vector backfill made no progress (${result.failed}/${result.attempted} failed)`
12484
+ };
12350
12485
  }
12486
+ const priorFailureStreak = vectorBackfillFailureStreak(job.payload);
12487
+ const payload = withoutVectorBackfillFailureStreak(job.payload);
12351
12488
  return {
12352
12489
  action: "reschedule",
12353
12490
  delayMs: result.attempted === 0 ? VECTOR_RETRY_DELAY_MS : 0,
12354
- resetAttempts: result.succeeded > 0
12491
+ resetAttempts: result.succeeded > 0,
12492
+ ...result.succeeded > 0 ? {
12493
+ ...priorFailureStreak > 0 ? { payload } : {},
12494
+ ...result.failed > 0 && result.lastError ? { lastError: result.lastError } : priorFailureStreak > 0 ? { clearLastError: true } : {}
12495
+ } : {}
12355
12496
  };
12356
12497
  };
12357
12498
  }