memorix 1.2.6 → 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.
Files changed (53) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +6 -3
  3. package/README.zh-CN.md +5 -2
  4. package/dist/cli/index.js +1272 -105
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/dashboard/static/app.js +7 -2
  7. package/dist/index.js +809 -52
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.d.ts +6 -0
  10. package/dist/maintenance-runner.js +204 -25
  11. package/dist/maintenance-runner.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +20 -0
  13. package/dist/sdk.d.ts +1 -1
  14. package/dist/sdk.js +809 -52
  15. package/dist/sdk.js.map +1 -1
  16. package/dist/types.d.ts +38 -1
  17. package/dist/types.js.map +1 -1
  18. package/docs/1.2.7-CONTEXT-CONTINUITY.md +68 -0
  19. package/docs/API_REFERENCE.md +6 -2
  20. package/docs/dev-log/progress.txt +48 -0
  21. package/docs/hooks-architecture.md +2 -1
  22. package/package.json +3 -3
  23. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  24. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  25. package/plugins/copilot/memorix/plugin.json +1 -1
  26. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  27. package/plugins/omp/memorix/extensions/memorix.js +17 -0
  28. package/plugins/omp/memorix/package.json +1 -1
  29. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  30. package/plugins/pi/memorix/extensions/memorix.js +17 -0
  31. package/plugins/pi/memorix/package.json +1 -1
  32. package/src/cli/capability-map.ts +1 -0
  33. package/src/cli/command-guide.ts +10 -0
  34. package/src/cli/commands/agent-integrations.ts +102 -5
  35. package/src/cli/commands/checkpoint.ts +144 -0
  36. package/src/cli/commands/serve-http.ts +14 -3
  37. package/src/cli/commands/setup.ts +44 -0
  38. package/src/cli/index.ts +3 -1
  39. package/src/codegraph/auto-context.ts +51 -5
  40. package/src/dashboard/server.ts +11 -0
  41. package/src/hooks/handler.ts +103 -11
  42. package/src/hooks/normalizer.ts +85 -1
  43. package/src/hooks/types.ts +16 -0
  44. package/src/knowledge/workset.ts +43 -2
  45. package/src/memory/compaction.ts +165 -0
  46. package/src/memory/observations.ts +67 -20
  47. package/src/runtime/maintenance-jobs.ts +84 -4
  48. package/src/runtime/project-maintenance.ts +63 -6
  49. package/src/server/tool-profile.ts +1 -0
  50. package/src/server.ts +94 -0
  51. package/src/store/compaction-checkpoint-store.ts +397 -0
  52. package/src/store/sqlite-db.ts +41 -0
  53. package/src/types.ts +46 -0
@@ -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 {
@@ -229,6 +229,7 @@ function getDatabase(dataDir) {
229
229
  db2.exec(CREATE_OBSERVATION_CODE_REFS_TABLE);
230
230
  db2.exec(CREATE_MAINTENANCE_JOBS_TABLE);
231
231
  db2.exec(CREATE_MAINTENANCE_TARGETS_TABLE);
232
+ db2.exec(CREATE_COMPACTION_CHECKPOINTS_TABLE);
232
233
  try {
233
234
  db2.exec(`ALTER TABLE mini_skills ADD COLUMN sourceSnapshot TEXT NOT NULL DEFAULT ''`);
234
235
  } catch {
@@ -278,7 +279,7 @@ function closeAllDatabases() {
278
279
  _dbCache.delete(key);
279
280
  }
280
281
  }
281
- var BetterSqlite3, CREATE_OBSERVATIONS_TABLE, CREATE_MINI_SKILLS_TABLE, CREATE_SESSIONS_TABLE, CREATE_META_TABLE, CREATE_TEAM_AGENTS_TABLE, CREATE_TEAM_MESSAGES_TABLE, CREATE_TEAM_TASKS_TABLE, CREATE_TEAM_TASK_DEPS_TABLE, CREATE_TEAM_LOCKS_TABLE, CREATE_TEAM_ROLES_TABLE, CREATE_CHAT_TRANSCRIPT_TABLE, CREATE_GRAPH_ENTITIES_TABLE, CREATE_GRAPH_RELATIONS_TABLE, CREATE_CODE_FILES_TABLE, CREATE_CODE_SYMBOLS_TABLE, CREATE_CODE_EDGES_TABLE, CREATE_OBSERVATION_CODE_REFS_TABLE, CREATE_SCHEMA_MIGRATIONS_TABLE, CREATE_CODE_STATE_SNAPSHOTS_TABLE, CREATE_KNOWLEDGE_CLAIMS_TABLE, CREATE_KNOWLEDGE_CLAIM_EVIDENCE_TABLE, CREATE_KNOWLEDGE_CLAIM_EVENTS_TABLE, CREATE_KNOWLEDGE_WORKSPACES_TABLE, CREATE_KNOWLEDGE_PAGES_TABLE, CREATE_KNOWLEDGE_PAGE_CLAIMS_TABLE, CREATE_KNOWLEDGE_PAGE_LINKS_TABLE, CREATE_KNOWLEDGE_PROPOSALS_TABLE, CREATE_KNOWLEDGE_WORKFLOWS_TABLE, CREATE_KNOWLEDGE_WORKFLOW_RUNS_TABLE, CREATE_MAINTENANCE_JOBS_TABLE, CREATE_MAINTENANCE_TARGETS_TABLE, CREATE_INDEXES, SCHEMA_MIGRATIONS, _dbCache;
282
+ var BetterSqlite3, CREATE_OBSERVATIONS_TABLE, CREATE_MINI_SKILLS_TABLE, CREATE_SESSIONS_TABLE, CREATE_META_TABLE, CREATE_TEAM_AGENTS_TABLE, CREATE_TEAM_MESSAGES_TABLE, CREATE_TEAM_TASKS_TABLE, CREATE_TEAM_TASK_DEPS_TABLE, CREATE_TEAM_LOCKS_TABLE, CREATE_TEAM_ROLES_TABLE, CREATE_CHAT_TRANSCRIPT_TABLE, CREATE_GRAPH_ENTITIES_TABLE, CREATE_GRAPH_RELATIONS_TABLE, CREATE_CODE_FILES_TABLE, CREATE_CODE_SYMBOLS_TABLE, CREATE_CODE_EDGES_TABLE, CREATE_OBSERVATION_CODE_REFS_TABLE, CREATE_SCHEMA_MIGRATIONS_TABLE, CREATE_CODE_STATE_SNAPSHOTS_TABLE, CREATE_KNOWLEDGE_CLAIMS_TABLE, CREATE_KNOWLEDGE_CLAIM_EVIDENCE_TABLE, CREATE_KNOWLEDGE_CLAIM_EVENTS_TABLE, CREATE_KNOWLEDGE_WORKSPACES_TABLE, CREATE_KNOWLEDGE_PAGES_TABLE, CREATE_KNOWLEDGE_PAGE_CLAIMS_TABLE, CREATE_KNOWLEDGE_PAGE_LINKS_TABLE, CREATE_KNOWLEDGE_PROPOSALS_TABLE, CREATE_KNOWLEDGE_WORKFLOWS_TABLE, CREATE_KNOWLEDGE_WORKFLOW_RUNS_TABLE, CREATE_MAINTENANCE_JOBS_TABLE, CREATE_MAINTENANCE_TARGETS_TABLE, CREATE_COMPACTION_CHECKPOINTS_TABLE, CREATE_INDEXES, SCHEMA_MIGRATIONS, _dbCache;
282
283
  var init_sqlite_db = __esm({
283
284
  "src/store/sqlite-db.ts"() {
284
285
  "use strict";
@@ -730,6 +731,31 @@ CREATE TABLE IF NOT EXISTS maintenance_targets (
730
731
  data_dir TEXT NOT NULL,
731
732
  updated_at INTEGER NOT NULL
732
733
  );
734
+ `;
735
+ CREATE_COMPACTION_CHECKPOINTS_TABLE = `
736
+ CREATE TABLE IF NOT EXISTS compaction_checkpoints (
737
+ id TEXT PRIMARY KEY,
738
+ project_id TEXT NOT NULL,
739
+ session_id TEXT NOT NULL,
740
+ agent TEXT NOT NULL,
741
+ phase TEXT NOT NULL,
742
+ capture_kind TEXT NOT NULL,
743
+ reason TEXT NOT NULL DEFAULT 'unknown',
744
+ source_event TEXT NOT NULL,
745
+ source_key TEXT NOT NULL,
746
+ summary TEXT,
747
+ tokens_before INTEGER,
748
+ first_kept_entry_id TEXT,
749
+ details_json TEXT NOT NULL DEFAULT '{}',
750
+ transcript_available INTEGER NOT NULL DEFAULT 0,
751
+ status TEXT NOT NULL DEFAULT 'active',
752
+ pre_captured_at TEXT NOT NULL,
753
+ completed_at TEXT,
754
+ delivered_at TEXT,
755
+ delivery_count INTEGER NOT NULL DEFAULT 0,
756
+ created_at TEXT NOT NULL,
757
+ updated_at TEXT NOT NULL
758
+ );
733
759
  `;
734
760
  CREATE_INDEXES = `
735
761
  CREATE INDEX IF NOT EXISTS idx_observations_projectId ON observations(projectId);
@@ -777,6 +803,9 @@ CREATE INDEX IF NOT EXISTS idx_knowledge_workflow_runs_project_workflow ON knowl
777
803
  CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_ready ON maintenance_jobs(status, run_after);
778
804
  CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_project ON maintenance_jobs(project_id, status, run_after);
779
805
  CREATE INDEX IF NOT EXISTS idx_maintenance_targets_updated ON maintenance_targets(updated_at);
806
+ CREATE INDEX IF NOT EXISTS idx_compaction_checkpoints_project_recent ON compaction_checkpoints(project_id, status, completed_at DESC, pre_captured_at DESC);
807
+ CREATE INDEX IF NOT EXISTS idx_compaction_checkpoints_session_pending ON compaction_checkpoints(project_id, session_id, agent, phase, status, pre_captured_at DESC);
808
+ CREATE INDEX IF NOT EXISTS idx_compaction_checkpoints_source ON compaction_checkpoints(project_id, source_key);
780
809
  CREATE UNIQUE INDEX IF NOT EXISTS idx_maintenance_jobs_active_dedupe
781
810
  ON maintenance_jobs(project_id, kind, dedupe_key)
782
811
  WHERE status IN ('pending', 'running', 'retry');
@@ -850,6 +879,15 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_maintenance_jobs_active_dedupe
850
879
  db2.exec("CREATE INDEX IF NOT EXISTS idx_knowledge_workflows_workspace_status ON knowledge_workflows(workspaceId, status, updatedAt DESC)");
851
880
  db2.exec("CREATE INDEX IF NOT EXISTS idx_knowledge_workflow_runs_project_workflow ON knowledge_workflow_runs(projectId, workflowId, startedAt DESC)");
852
881
  }
882
+ },
883
+ {
884
+ id: "1.2.7-compaction-checkpoints",
885
+ apply: (db2) => {
886
+ db2.exec(CREATE_COMPACTION_CHECKPOINTS_TABLE);
887
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_compaction_checkpoints_project_recent ON compaction_checkpoints(project_id, status, completed_at DESC, pre_captured_at DESC)");
888
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_compaction_checkpoints_session_pending ON compaction_checkpoints(project_id, session_id, agent, phase, status, pre_captured_at DESC)");
889
+ db2.exec("CREATE INDEX IF NOT EXISTS idx_compaction_checkpoints_source ON compaction_checkpoints(project_id, source_key)");
890
+ }
853
891
  }
854
892
  ];
855
893
  _dbCache = /* @__PURE__ */ new Map();
@@ -5880,6 +5918,10 @@ var init_maintenance_jobs = __esm({
5880
5918
  LIMIT 1
5881
5919
  `).get(input.projectId, input.kind, dedupeKey);
5882
5920
  if (existing) {
5921
+ if (input.kind === "vector-backfill" && existing.status === "retry") {
5922
+ this.commit();
5923
+ return rowToJob(existing);
5924
+ }
5883
5925
  const nextRunAfter = Math.min(Number(existing.run_after), runAfter);
5884
5926
  this.db.prepare(`
5885
5927
  UPDATE maintenance_jobs
@@ -5890,6 +5932,26 @@ var init_maintenance_jobs = __esm({
5890
5932
  this.commit();
5891
5933
  return rowToJob(updated);
5892
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
+ }
5893
5955
  const id = randomUUID2();
5894
5956
  this.db.prepare(`
5895
5957
  INSERT INTO maintenance_jobs (
@@ -6061,15 +6123,42 @@ var init_maintenance_jobs = __esm({
6061
6123
  const now3 = options.now ?? Date.now();
6062
6124
  const delayMs = Number.isFinite(options.delayMs) ? Math.max(0, Math.floor(options.delayMs)) : 0;
6063
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;
6064
6129
  this.db.prepare(`
6065
6130
  UPDATE maintenance_jobs
6066
- 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,
6067
6132
  payload_json = COALESCE(?, payload_json),
6133
+ last_error = CASE
6134
+ WHEN ? THEN NULL
6135
+ WHEN ? THEN ?
6136
+ ELSE last_error
6137
+ END,
6068
6138
  lease_owner = NULL, lease_expires_at = NULL, updated_at = ?
6069
6139
  WHERE id = ? AND status = 'running' AND lease_owner = ?
6070
- `).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
+ );
6071
6152
  return this.get(id);
6072
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
+ }
6073
6162
  fail(id, workerId, error, now3 = Date.now()) {
6074
6163
  this.begin();
6075
6164
  try {
@@ -6165,6 +6254,9 @@ var init_maintenance_jobs = __esm({
6165
6254
  delayMs: result.delayMs,
6166
6255
  resetAttempts: result.resetAttempts,
6167
6256
  payload: result.payload,
6257
+ status: result.status,
6258
+ lastError: result.lastError,
6259
+ clearLastError: result.clearLastError,
6168
6260
  now: now3
6169
6261
  });
6170
6262
  return { state: "rescheduled", job: updated2 };
@@ -7124,6 +7216,9 @@ function normalizeEmbeddingFailure(error) {
7124
7216
  message: raw
7125
7217
  };
7126
7218
  }
7219
+ function vectorBackfillError(error) {
7220
+ return sanitizeCredentials(normalizeEmbeddingFailure(error).message).slice(0, 1e3);
7221
+ }
7127
7222
  function queueVectorBackfill(projectId) {
7128
7223
  const dataDir = projectDir;
7129
7224
  if (!dataDir) return;
@@ -7879,17 +7974,53 @@ async function backfillVectorEmbeddings(options = {}) {
7879
7974
  let succeeded = 0;
7880
7975
  let failed = 0;
7881
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
+ }
7882
7993
  try {
7883
- for (const id of ids) {
7884
- const obs = observationById.get(id);
7885
- if (!obs) {
7886
- vectorMissingIds.delete(id);
7887
- 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));
7888
8009
  }
7889
- const text = [obs.title, obs.narrative, ...obs.facts].join(" ");
7890
- try {
7891
- const embedding = await generateEmbedding(text);
7892
- 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 {
7893
8024
  if (!isVectorCompatibleWithCurrentIndex(embedding)) {
7894
8025
  await upgradeVectorSchemaAfterFirstEmbedding(embedding);
7895
8026
  }
@@ -7898,7 +8029,7 @@ async function backfillVectorEmbeddings(options = {}) {
7898
8029
  console.error(
7899
8030
  `[memorix] Backfill embedding mismatch for obs-${id}: provider returned ${embedding.length}d, index expects ${vectorDimensions ?? "unknown"}d (kept in queue)`
7900
8031
  );
7901
- 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`);
7902
8033
  failed++;
7903
8034
  continue;
7904
8035
  }
@@ -7937,15 +8068,10 @@ async function backfillVectorEmbeddings(options = {}) {
7937
8068
  await insertObservation(doc);
7938
8069
  vectorMissingIds.delete(id);
7939
8070
  succeeded++;
7940
- } else if (isEmbeddingExplicitlyDisabled()) {
7941
- vectorMissingIds.delete(id);
7942
- } else {
7943
- lastFailure = "embedding provider unavailable";
8071
+ } catch (error) {
8072
+ recordFailure(vectorBackfillError(error));
7944
8073
  failed++;
7945
8074
  }
7946
- } catch (err) {
7947
- lastFailure = err instanceof Error ? err.message : String(err);
7948
- failed++;
7949
8075
  }
7950
8076
  }
7951
8077
  } finally {
@@ -7958,7 +8084,12 @@ async function backfillVectorEmbeddings(options = {}) {
7958
8084
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
7959
8085
  };
7960
8086
  }
7961
- return { attempted: ids.length, succeeded, failed };
8087
+ return {
8088
+ attempted: ids.length,
8089
+ succeeded,
8090
+ failed,
8091
+ ...lastFailure ? { lastError: lastFailure } : {}
8092
+ };
7962
8093
  }
7963
8094
  var observations, nextId, projectDir, searchIndexPrepared, vectorMissingIds, vectorBackfillRunning, vectorSchemaUpgradePromise, lastVectorBackfill, embeddingFailureLogTimestamps, EMBEDDING_FAILURE_LOG_COOLDOWN_MS;
7964
8095
  var init_observations = __esm({
@@ -11877,6 +12008,7 @@ var DEFAULT_TIMEOUT_MS = 5 * 6e4;
11877
12008
 
11878
12009
  // src/runtime/project-maintenance.ts
11879
12010
  init_esm_shims();
12011
+ init_maintenance_jobs();
11880
12012
  init_lifecycle();
11881
12013
  init_timeout();
11882
12014
  var DEFAULT_VECTOR_BATCH_SIZE = 12;
@@ -11886,12 +12018,34 @@ var DEFAULT_CODEGRAPH_MAX_FILES = 5e3;
11886
12018
  var DEFAULT_CLAIM_DERIVATION_BATCH_SIZE = 100;
11887
12019
  var DEFAULT_OBSERVATION_QUALIFICATION_BATCH_SIZE = 100;
11888
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;
11889
12023
  var DEDUP_PER_PAIR_TIMEOUT_MS = 5e3;
11890
12024
  function vectorBatchSize(payload) {
11891
12025
  const value = payload.limit;
11892
12026
  if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_VECTOR_BATCH_SIZE;
11893
12027
  return Math.min(100, Math.max(1, Math.floor(value)));
11894
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
+ }
11895
12049
  function retentionBatchSize(payload) {
11896
12050
  const value = payload.limit;
11897
12051
  if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_RETENTION_BATCH_SIZE;
@@ -12300,20 +12454,45 @@ function createProjectMaintenanceHandler(projectId, projectDir2, projectRoot, op
12300
12454
  if (isEmbeddingExplicitlyDisabled2()) return { action: "complete" };
12301
12455
  const { backfillVectorEmbeddings: backfillVectorEmbeddings2, getVectorStatus: getVectorStatus2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
12302
12456
  const before = getVectorStatus2(projectId);
12303
- if (before.missing === 0) return { action: "complete" };
12457
+ if (before.missing === 0) {
12458
+ resolveSupersededVectorFailures(projectDir2, projectId);
12459
+ return { action: "complete" };
12460
+ }
12304
12461
  const result = await backfillVectorEmbeddings2({
12305
12462
  projectId,
12306
12463
  limit: vectorBatchSize(job.payload)
12307
12464
  });
12308
12465
  const after = getVectorStatus2(projectId);
12309
- if (after.missing === 0) return { action: "complete" };
12466
+ if (after.missing === 0) {
12467
+ resolveSupersededVectorFailures(projectDir2, projectId);
12468
+ return { action: "complete" };
12469
+ }
12310
12470
  if (result.failed > 0 && result.succeeded === 0) {
12311
- 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
+ };
12312
12485
  }
12486
+ const priorFailureStreak = vectorBackfillFailureStreak(job.payload);
12487
+ const payload = withoutVectorBackfillFailureStreak(job.payload);
12313
12488
  return {
12314
12489
  action: "reschedule",
12315
12490
  delayMs: result.attempted === 0 ? VECTOR_RETRY_DELAY_MS : 0,
12316
- 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
+ } : {}
12317
12496
  };
12318
12497
  };
12319
12498
  }