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.
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.2.8] - 2026-07-27
6
+
7
+ ### Fixed
8
+ - **Recoverable vector backfill** -- Temporary embedding or index failures no longer exhaust a shared background job. Memorix keeps one diagnosable retry with bounded backoff, batches provider requests, and lets a later healthy MCP session resume the same recovery work instead of accumulating permanent failures.
9
+ - **Accurate vector status outside MCP** -- The standalone dashboard and HTTP control plane no longer present an unhydrated in-memory index as a healthy `0 / 0` result. They now explicitly say that vector status belongs to the active MCP session when they cannot observe it.
10
+ - **Codex plugin ownership** -- Agent Doctor recognizes the enabled Codex plugin as the owner of `memorix serve`, reports first-use hook approval as a host consent step instead of a broken install, and avoids suggesting a redundant config repair.
11
+ - **Safe Codex migration** -- `memorix setup --agent codex --global` removes only the known old source-path Memorix MCP entry after the official plugin installation succeeds. Custom user-managed MCP entries remain untouched.
12
+
5
13
  ## [1.2.7] - 2026-07-27
6
14
 
7
15
  ### Added
package/dist/sdk.js CHANGED
@@ -7185,6 +7185,10 @@ var init_maintenance_jobs = __esm({
7185
7185
  LIMIT 1
7186
7186
  `).get(input.projectId, input.kind, dedupeKey);
7187
7187
  if (existing) {
7188
+ if (input.kind === "vector-backfill" && existing.status === "retry") {
7189
+ this.commit();
7190
+ return rowToJob(existing);
7191
+ }
7188
7192
  const nextRunAfter = Math.min(Number(existing.run_after), runAfter);
7189
7193
  this.db.prepare(`
7190
7194
  UPDATE maintenance_jobs
@@ -7195,6 +7199,26 @@ var init_maintenance_jobs = __esm({
7195
7199
  this.commit();
7196
7200
  return rowToJob(updated);
7197
7201
  }
7202
+ if (input.kind === "vector-backfill") {
7203
+ const failed = this.db.prepare(`
7204
+ SELECT * FROM maintenance_jobs
7205
+ WHERE project_id = ? AND kind = ? AND dedupe_key = ? AND status = 'failed'
7206
+ ORDER BY updated_at DESC
7207
+ LIMIT 1
7208
+ `).get(input.projectId, input.kind, dedupeKey);
7209
+ if (failed) {
7210
+ this.db.prepare(`
7211
+ UPDATE maintenance_jobs
7212
+ SET status = 'pending', attempts = 0, max_attempts = ?, run_after = ?,
7213
+ payload_json = ?, lease_owner = NULL, lease_expires_at = NULL,
7214
+ last_error = NULL, completed_at = NULL, updated_at = ?
7215
+ WHERE id = ?
7216
+ `).run(maxAttempts, runAfter, payloadJson, now3, failed.id);
7217
+ const revived = this.getRow(failed.id);
7218
+ this.commit();
7219
+ return rowToJob(revived);
7220
+ }
7221
+ }
7198
7222
  const id = randomUUID2();
7199
7223
  this.db.prepare(`
7200
7224
  INSERT INTO maintenance_jobs (
@@ -7366,15 +7390,42 @@ var init_maintenance_jobs = __esm({
7366
7390
  const now3 = options.now ?? Date.now();
7367
7391
  const delayMs = Number.isFinite(options.delayMs) ? Math.max(0, Math.floor(options.delayMs)) : 0;
7368
7392
  const payloadJson = options.payload === void 0 ? null : JSON.stringify(options.payload);
7393
+ const status = options.status === "retry" ? "retry" : "pending";
7394
+ const hasLastError = options.lastError !== void 0;
7395
+ const lastError = hasLastError ? errorText(options.lastError) : null;
7369
7396
  this.db.prepare(`
7370
7397
  UPDATE maintenance_jobs
7371
- SET status = 'pending', run_after = ?, attempts = CASE WHEN ? THEN 0 ELSE attempts END,
7398
+ SET status = ?, run_after = ?, attempts = CASE WHEN ? THEN 0 ELSE attempts END,
7372
7399
  payload_json = COALESCE(?, payload_json),
7400
+ last_error = CASE
7401
+ WHEN ? THEN NULL
7402
+ WHEN ? THEN ?
7403
+ ELSE last_error
7404
+ END,
7373
7405
  lease_owner = NULL, lease_expires_at = NULL, updated_at = ?
7374
7406
  WHERE id = ? AND status = 'running' AND lease_owner = ?
7375
- `).run(now3 + delayMs, options.resetAttempts ? 1 : 0, payloadJson, now3, id, workerId);
7407
+ `).run(
7408
+ status,
7409
+ now3 + delayMs,
7410
+ options.resetAttempts ? 1 : 0,
7411
+ payloadJson,
7412
+ options.clearLastError ? 1 : 0,
7413
+ hasLastError ? 1 : 0,
7414
+ lastError,
7415
+ now3,
7416
+ id,
7417
+ workerId
7418
+ );
7376
7419
  return this.get(id);
7377
7420
  }
7421
+ resolveFailedVectorBackfills(projectId, dedupeKey = "vector-backfill", now3 = Date.now()) {
7422
+ const result = this.db.prepare(`
7423
+ UPDATE maintenance_jobs
7424
+ SET status = 'completed', completed_at = ?, updated_at = ?
7425
+ WHERE project_id = ? AND kind = 'vector-backfill' AND dedupe_key = ? AND status = 'failed'
7426
+ `).run(now3, now3, projectId, dedupeKey);
7427
+ return Number(result.changes ?? 0);
7428
+ }
7378
7429
  fail(id, workerId, error, now3 = Date.now()) {
7379
7430
  this.begin();
7380
7431
  try {
@@ -7470,6 +7521,9 @@ var init_maintenance_jobs = __esm({
7470
7521
  delayMs: result.delayMs,
7471
7522
  resetAttempts: result.resetAttempts,
7472
7523
  payload: result.payload,
7524
+ status: result.status,
7525
+ lastError: result.lastError,
7526
+ clearLastError: result.clearLastError,
7473
7527
  now: now3
7474
7528
  });
7475
7529
  return { state: "rescheduled", job: updated2 };
@@ -8448,6 +8502,9 @@ function normalizeEmbeddingFailure(error) {
8448
8502
  message: raw
8449
8503
  };
8450
8504
  }
8505
+ function vectorBackfillError(error) {
8506
+ return sanitizeCredentials(normalizeEmbeddingFailure(error).message).slice(0, 1e3);
8507
+ }
8451
8508
  function queueVectorBackfill(projectId) {
8452
8509
  const dataDir = projectDir;
8453
8510
  if (!dataDir) return;
@@ -9203,17 +9260,53 @@ async function backfillVectorEmbeddings(options = {}) {
9203
9260
  let succeeded = 0;
9204
9261
  let failed = 0;
9205
9262
  let lastFailure;
9263
+ const recordFailure = (message) => {
9264
+ if (!lastFailure) lastFailure = message;
9265
+ };
9266
+ const candidates = [];
9267
+ for (const id of ids) {
9268
+ const observation = observationById.get(id);
9269
+ if (!observation) {
9270
+ vectorMissingIds.delete(id);
9271
+ continue;
9272
+ }
9273
+ candidates.push({
9274
+ id,
9275
+ observation,
9276
+ text: [observation.title, observation.narrative, ...observation.facts].join(" ")
9277
+ });
9278
+ }
9206
9279
  try {
9207
- for (const id of ids) {
9208
- const obs = observationById.get(id);
9209
- if (!obs) {
9210
- vectorMissingIds.delete(id);
9211
- continue;
9280
+ if (candidates.length === 0) {
9281
+ return { attempted: ids.length, succeeded, failed };
9282
+ }
9283
+ let embeddings = null;
9284
+ try {
9285
+ const provider2 = await getEmbeddingProvider();
9286
+ if (!provider2) {
9287
+ if (isEmbeddingExplicitlyDisabled()) {
9288
+ for (const candidate of candidates) vectorMissingIds.delete(candidate.id);
9289
+ } else {
9290
+ recordFailure("embedding provider unavailable");
9291
+ failed += candidates.length;
9292
+ }
9293
+ } else {
9294
+ embeddings = await provider2.embedBatch(candidates.map((candidate) => candidate.text));
9212
9295
  }
9213
- const text = [obs.title, obs.narrative, ...obs.facts].join(" ");
9214
- try {
9215
- const embedding = await generateEmbedding(text);
9216
- if (embedding) {
9296
+ } catch (error) {
9297
+ recordFailure(vectorBackfillError(error));
9298
+ failed += candidates.length;
9299
+ }
9300
+ if (embeddings) {
9301
+ for (let index = 0; index < candidates.length; index++) {
9302
+ const { id, observation: obs } = candidates[index];
9303
+ const embedding = embeddings[index];
9304
+ if (!embedding || embedding.length === 0) {
9305
+ recordFailure("embedding provider returned no vector");
9306
+ failed++;
9307
+ continue;
9308
+ }
9309
+ try {
9217
9310
  if (!isVectorCompatibleWithCurrentIndex(embedding)) {
9218
9311
  await upgradeVectorSchemaAfterFirstEmbedding(embedding);
9219
9312
  }
@@ -9222,7 +9315,7 @@ async function backfillVectorEmbeddings(options = {}) {
9222
9315
  console.error(
9223
9316
  `[memorix] Backfill embedding mismatch for obs-${id}: provider returned ${embedding.length}d, index expects ${vectorDimensions ?? "unknown"}d (kept in queue)`
9224
9317
  );
9225
- lastFailure = `dimension mismatch: provider returned ${embedding.length}d, index expects ${vectorDimensions ?? "unknown"}d`;
9318
+ recordFailure(`dimension mismatch: provider returned ${embedding.length}d, index expects ${vectorDimensions ?? "unknown"}d`);
9226
9319
  failed++;
9227
9320
  continue;
9228
9321
  }
@@ -9261,15 +9354,10 @@ async function backfillVectorEmbeddings(options = {}) {
9261
9354
  await insertObservation(doc);
9262
9355
  vectorMissingIds.delete(id);
9263
9356
  succeeded++;
9264
- } else if (isEmbeddingExplicitlyDisabled()) {
9265
- vectorMissingIds.delete(id);
9266
- } else {
9267
- lastFailure = "embedding provider unavailable";
9357
+ } catch (error) {
9358
+ recordFailure(vectorBackfillError(error));
9268
9359
  failed++;
9269
9360
  }
9270
- } catch (err) {
9271
- lastFailure = err instanceof Error ? err.message : String(err);
9272
- failed++;
9273
9361
  }
9274
9362
  }
9275
9363
  } finally {
@@ -9282,7 +9370,12 @@ async function backfillVectorEmbeddings(options = {}) {
9282
9370
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
9283
9371
  };
9284
9372
  }
9285
- return { attempted: ids.length, succeeded, failed };
9373
+ return {
9374
+ attempted: ids.length,
9375
+ succeeded,
9376
+ failed,
9377
+ ...lastFailure ? { lastError: lastFailure } : {}
9378
+ };
9286
9379
  }
9287
9380
  var observations, nextId, projectDir, searchIndexPrepared, vectorMissingIds, vectorBackfillRunning, vectorSchemaUpgradePromise, lastVectorBackfill, embeddingFailureLogTimestamps, EMBEDDING_FAILURE_LOG_COOLDOWN_MS;
9288
9381
  var init_observations = __esm({
@@ -14919,6 +15012,26 @@ function vectorBatchSize(payload) {
14919
15012
  if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_VECTOR_BATCH_SIZE;
14920
15013
  return Math.min(100, Math.max(1, Math.floor(value)));
14921
15014
  }
15015
+ function vectorBackfillFailureStreak(payload) {
15016
+ const value = payload.vectorBackfillFailureStreak;
15017
+ return typeof value === "number" && Number.isFinite(value) ? Math.min(16, Math.max(0, Math.floor(value))) : 0;
15018
+ }
15019
+ function vectorBackfillRetryDelayMs(streak) {
15020
+ return Math.min(
15021
+ VECTOR_FAILURE_MAX_RETRY_DELAY_MS,
15022
+ VECTOR_FAILURE_BASE_RETRY_DELAY_MS * 2 ** Math.max(0, streak - 1)
15023
+ );
15024
+ }
15025
+ function withoutVectorBackfillFailureStreak(payload) {
15026
+ const { vectorBackfillFailureStreak: _streak, ...rest } = payload;
15027
+ return rest;
15028
+ }
15029
+ function resolveSupersededVectorFailures(projectDir2, projectId) {
15030
+ try {
15031
+ new MaintenanceJobStore(projectDir2).resolveFailedVectorBackfills(projectId);
15032
+ } catch {
15033
+ }
15034
+ }
14922
15035
  function retentionBatchSize(payload) {
14923
15036
  const value = payload.limit;
14924
15037
  if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_RETENTION_BATCH_SIZE;
@@ -15327,20 +15440,45 @@ function createProjectMaintenanceHandler(projectId, projectDir2, projectRoot, op
15327
15440
  if (isEmbeddingExplicitlyDisabled2()) return { action: "complete" };
15328
15441
  const { backfillVectorEmbeddings: backfillVectorEmbeddings2, getVectorStatus: getVectorStatus2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
15329
15442
  const before = getVectorStatus2(projectId);
15330
- if (before.missing === 0) return { action: "complete" };
15443
+ if (before.missing === 0) {
15444
+ resolveSupersededVectorFailures(projectDir2, projectId);
15445
+ return { action: "complete" };
15446
+ }
15331
15447
  const result = await backfillVectorEmbeddings2({
15332
15448
  projectId,
15333
15449
  limit: vectorBatchSize(job.payload)
15334
15450
  });
15335
15451
  const after = getVectorStatus2(projectId);
15336
- if (after.missing === 0) return { action: "complete" };
15452
+ if (after.missing === 0) {
15453
+ resolveSupersededVectorFailures(projectDir2, projectId);
15454
+ return { action: "complete" };
15455
+ }
15337
15456
  if (result.failed > 0 && result.succeeded === 0) {
15338
- throw new Error(`vector backfill made no progress (${result.failed}/${result.attempted} failed)`);
15457
+ const streak = vectorBackfillFailureStreak(job.payload) + 1;
15458
+ return {
15459
+ action: "reschedule",
15460
+ status: "retry",
15461
+ delayMs: vectorBackfillRetryDelayMs(streak),
15462
+ // This is a recoverable provider/index state, not a terminal job error.
15463
+ // Keeping one retry row prevents new MCP processes from creating a storm.
15464
+ resetAttempts: true,
15465
+ payload: {
15466
+ ...job.payload,
15467
+ vectorBackfillFailureStreak: streak
15468
+ },
15469
+ lastError: result.lastError ?? `vector backfill made no progress (${result.failed}/${result.attempted} failed)`
15470
+ };
15339
15471
  }
15472
+ const priorFailureStreak = vectorBackfillFailureStreak(job.payload);
15473
+ const payload = withoutVectorBackfillFailureStreak(job.payload);
15340
15474
  return {
15341
15475
  action: "reschedule",
15342
15476
  delayMs: result.attempted === 0 ? VECTOR_RETRY_DELAY_MS : 0,
15343
- resetAttempts: result.succeeded > 0
15477
+ resetAttempts: result.succeeded > 0,
15478
+ ...result.succeeded > 0 ? {
15479
+ ...priorFailureStreak > 0 ? { payload } : {},
15480
+ ...result.failed > 0 && result.lastError ? { lastError: result.lastError } : priorFailureStreak > 0 ? { clearLastError: true } : {}
15481
+ } : {}
15344
15482
  };
15345
15483
  };
15346
15484
  }
@@ -15353,11 +15491,12 @@ function createProjectMaintenanceDispatcher(projectId, projectDir2, projectRoot,
15353
15491
  return isolatedRunner({ job, projectRoot, dataDir: projectDir2 });
15354
15492
  };
15355
15493
  }
15356
- var DEFAULT_VECTOR_BATCH_SIZE, DEFAULT_RETENTION_BATCH_SIZE, DEFAULT_CONSOLIDATION_BATCH_SIZE, DEFAULT_CODEGRAPH_MAX_FILES, DEFAULT_CLAIM_DERIVATION_BATCH_SIZE, DEFAULT_OBSERVATION_QUALIFICATION_BATCH_SIZE, VECTOR_RETRY_DELAY_MS, DEDUP_PER_PAIR_TIMEOUT_MS;
15494
+ var DEFAULT_VECTOR_BATCH_SIZE, DEFAULT_RETENTION_BATCH_SIZE, DEFAULT_CONSOLIDATION_BATCH_SIZE, DEFAULT_CODEGRAPH_MAX_FILES, DEFAULT_CLAIM_DERIVATION_BATCH_SIZE, DEFAULT_OBSERVATION_QUALIFICATION_BATCH_SIZE, VECTOR_RETRY_DELAY_MS, VECTOR_FAILURE_BASE_RETRY_DELAY_MS, VECTOR_FAILURE_MAX_RETRY_DELAY_MS, DEDUP_PER_PAIR_TIMEOUT_MS;
15357
15495
  var init_project_maintenance = __esm({
15358
15496
  "src/runtime/project-maintenance.ts"() {
15359
15497
  "use strict";
15360
15498
  init_esm_shims();
15499
+ init_maintenance_jobs();
15361
15500
  init_isolated_maintenance();
15362
15501
  init_lifecycle();
15363
15502
  init_timeout();
@@ -15368,6 +15507,8 @@ var init_project_maintenance = __esm({
15368
15507
  DEFAULT_CLAIM_DERIVATION_BATCH_SIZE = 100;
15369
15508
  DEFAULT_OBSERVATION_QUALIFICATION_BATCH_SIZE = 100;
15370
15509
  VECTOR_RETRY_DELAY_MS = 5e3;
15510
+ VECTOR_FAILURE_BASE_RETRY_DELAY_MS = 6e4;
15511
+ VECTOR_FAILURE_MAX_RETRY_DELAY_MS = 15 * 6e4;
15371
15512
  DEDUP_PER_PAIR_TIMEOUT_MS = 5e3;
15372
15513
  }
15373
15514
  });
@@ -20702,6 +20843,17 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
20702
20843
  sourceCounts,
20703
20844
  recentObservations: sorted,
20704
20845
  embedding: embeddingStatus,
20846
+ // A standalone dashboard has no access to an active MCP
20847
+ // process's in-memory Orama index. Keep this explicit so the
20848
+ // UI never presents an empty local singleton as a healthy
20849
+ // all-vectors-indexed result.
20850
+ vectorStatus: {
20851
+ available: false,
20852
+ total: 0,
20853
+ missing: 0,
20854
+ missingIds: [],
20855
+ backfillRunning: false
20856
+ },
20705
20857
  storage: storageInfo,
20706
20858
  maintenance,
20707
20859
  ...lifecycle ? { lifecycle } : {},
@@ -27774,7 +27926,7 @@ The path should point to a directory containing a .git folder.`
27774
27926
  };
27775
27927
  const server = existingServer ?? new McpServer({
27776
27928
  name: "memorix",
27777
- version: true ? "1.2.7" : "1.0.1"
27929
+ version: true ? "1.2.8" : "1.0.1"
27778
27930
  });
27779
27931
  const originalRegisterTool = server.registerTool.bind(server);
27780
27932
  server.registerTool = ((name, ...args) => {