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.
package/CHANGELOG.md CHANGED
@@ -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/cli/index.js CHANGED
@@ -3212,7 +3212,7 @@ ${import_picocolors.default.gray(m3)} ${s2}
3212
3212
 
3213
3213
  // src/cli/version.ts
3214
3214
  function getCliVersion() {
3215
- return true ? "1.2.7" : pkg.version;
3215
+ return true ? "1.2.8" : pkg.version;
3216
3216
  }
3217
3217
  var init_version = __esm({
3218
3218
  "src/cli/version.ts"() {
@@ -17184,6 +17184,10 @@ var init_maintenance_jobs = __esm({
17184
17184
  LIMIT 1
17185
17185
  `).get(input.projectId, input.kind, dedupeKey);
17186
17186
  if (existing) {
17187
+ if (input.kind === "vector-backfill" && existing.status === "retry") {
17188
+ this.commit();
17189
+ return rowToJob(existing);
17190
+ }
17187
17191
  const nextRunAfter = Math.min(Number(existing.run_after), runAfter);
17188
17192
  this.db.prepare(`
17189
17193
  UPDATE maintenance_jobs
@@ -17194,6 +17198,26 @@ var init_maintenance_jobs = __esm({
17194
17198
  this.commit();
17195
17199
  return rowToJob(updated);
17196
17200
  }
17201
+ if (input.kind === "vector-backfill") {
17202
+ const failed = this.db.prepare(`
17203
+ SELECT * FROM maintenance_jobs
17204
+ WHERE project_id = ? AND kind = ? AND dedupe_key = ? AND status = 'failed'
17205
+ ORDER BY updated_at DESC
17206
+ LIMIT 1
17207
+ `).get(input.projectId, input.kind, dedupeKey);
17208
+ if (failed) {
17209
+ this.db.prepare(`
17210
+ UPDATE maintenance_jobs
17211
+ SET status = 'pending', attempts = 0, max_attempts = ?, run_after = ?,
17212
+ payload_json = ?, lease_owner = NULL, lease_expires_at = NULL,
17213
+ last_error = NULL, completed_at = NULL, updated_at = ?
17214
+ WHERE id = ?
17215
+ `).run(maxAttempts, runAfter, payloadJson, now3, failed.id);
17216
+ const revived = this.getRow(failed.id);
17217
+ this.commit();
17218
+ return rowToJob(revived);
17219
+ }
17220
+ }
17197
17221
  const id = randomUUID2();
17198
17222
  this.db.prepare(`
17199
17223
  INSERT INTO maintenance_jobs (
@@ -17365,15 +17389,42 @@ var init_maintenance_jobs = __esm({
17365
17389
  const now3 = options2.now ?? Date.now();
17366
17390
  const delayMs = Number.isFinite(options2.delayMs) ? Math.max(0, Math.floor(options2.delayMs)) : 0;
17367
17391
  const payloadJson = options2.payload === void 0 ? null : JSON.stringify(options2.payload);
17392
+ const status = options2.status === "retry" ? "retry" : "pending";
17393
+ const hasLastError = options2.lastError !== void 0;
17394
+ const lastError = hasLastError ? errorText(options2.lastError) : null;
17368
17395
  this.db.prepare(`
17369
17396
  UPDATE maintenance_jobs
17370
- SET status = 'pending', run_after = ?, attempts = CASE WHEN ? THEN 0 ELSE attempts END,
17397
+ SET status = ?, run_after = ?, attempts = CASE WHEN ? THEN 0 ELSE attempts END,
17371
17398
  payload_json = COALESCE(?, payload_json),
17399
+ last_error = CASE
17400
+ WHEN ? THEN NULL
17401
+ WHEN ? THEN ?
17402
+ ELSE last_error
17403
+ END,
17372
17404
  lease_owner = NULL, lease_expires_at = NULL, updated_at = ?
17373
17405
  WHERE id = ? AND status = 'running' AND lease_owner = ?
17374
- `).run(now3 + delayMs, options2.resetAttempts ? 1 : 0, payloadJson, now3, id, workerId);
17406
+ `).run(
17407
+ status,
17408
+ now3 + delayMs,
17409
+ options2.resetAttempts ? 1 : 0,
17410
+ payloadJson,
17411
+ options2.clearLastError ? 1 : 0,
17412
+ hasLastError ? 1 : 0,
17413
+ lastError,
17414
+ now3,
17415
+ id,
17416
+ workerId
17417
+ );
17375
17418
  return this.get(id);
17376
17419
  }
17420
+ resolveFailedVectorBackfills(projectId, dedupeKey = "vector-backfill", now3 = Date.now()) {
17421
+ const result = this.db.prepare(`
17422
+ UPDATE maintenance_jobs
17423
+ SET status = 'completed', completed_at = ?, updated_at = ?
17424
+ WHERE project_id = ? AND kind = 'vector-backfill' AND dedupe_key = ? AND status = 'failed'
17425
+ `).run(now3, now3, projectId, dedupeKey);
17426
+ return Number(result.changes ?? 0);
17427
+ }
17377
17428
  fail(id, workerId, error2, now3 = Date.now()) {
17378
17429
  this.begin();
17379
17430
  try {
@@ -17469,6 +17520,9 @@ var init_maintenance_jobs = __esm({
17469
17520
  delayMs: result.delayMs,
17470
17521
  resetAttempts: result.resetAttempts,
17471
17522
  payload: result.payload,
17523
+ status: result.status,
17524
+ lastError: result.lastError,
17525
+ clearLastError: result.clearLastError,
17472
17526
  now: now3
17473
17527
  });
17474
17528
  return { state: "rescheduled", job: updated2 };
@@ -18447,6 +18501,9 @@ function normalizeEmbeddingFailure(error2) {
18447
18501
  message: raw
18448
18502
  };
18449
18503
  }
18504
+ function vectorBackfillError(error2) {
18505
+ return sanitizeCredentials(normalizeEmbeddingFailure(error2).message).slice(0, 1e3);
18506
+ }
18450
18507
  function queueVectorBackfill(projectId) {
18451
18508
  const dataDir = projectDir;
18452
18509
  if (!dataDir) return;
@@ -19202,17 +19259,53 @@ async function backfillVectorEmbeddings(options2 = {}) {
19202
19259
  let succeeded = 0;
19203
19260
  let failed = 0;
19204
19261
  let lastFailure;
19262
+ const recordFailure = (message) => {
19263
+ if (!lastFailure) lastFailure = message;
19264
+ };
19265
+ const candidates = [];
19266
+ for (const id of ids) {
19267
+ const observation = observationById.get(id);
19268
+ if (!observation) {
19269
+ vectorMissingIds.delete(id);
19270
+ continue;
19271
+ }
19272
+ candidates.push({
19273
+ id,
19274
+ observation,
19275
+ text: [observation.title, observation.narrative, ...observation.facts].join(" ")
19276
+ });
19277
+ }
19205
19278
  try {
19206
- for (const id of ids) {
19207
- const obs = observationById.get(id);
19208
- if (!obs) {
19209
- vectorMissingIds.delete(id);
19210
- continue;
19279
+ if (candidates.length === 0) {
19280
+ return { attempted: ids.length, succeeded, failed };
19281
+ }
19282
+ let embeddings = null;
19283
+ try {
19284
+ const provider2 = await getEmbeddingProvider();
19285
+ if (!provider2) {
19286
+ if (isEmbeddingExplicitlyDisabled()) {
19287
+ for (const candidate2 of candidates) vectorMissingIds.delete(candidate2.id);
19288
+ } else {
19289
+ recordFailure("embedding provider unavailable");
19290
+ failed += candidates.length;
19291
+ }
19292
+ } else {
19293
+ embeddings = await provider2.embedBatch(candidates.map((candidate2) => candidate2.text));
19211
19294
  }
19212
- const text = [obs.title, obs.narrative, ...obs.facts].join(" ");
19213
- try {
19214
- const embedding = await generateEmbedding(text);
19215
- if (embedding) {
19295
+ } catch (error2) {
19296
+ recordFailure(vectorBackfillError(error2));
19297
+ failed += candidates.length;
19298
+ }
19299
+ if (embeddings) {
19300
+ for (let index = 0; index < candidates.length; index++) {
19301
+ const { id, observation: obs } = candidates[index];
19302
+ const embedding = embeddings[index];
19303
+ if (!embedding || embedding.length === 0) {
19304
+ recordFailure("embedding provider returned no vector");
19305
+ failed++;
19306
+ continue;
19307
+ }
19308
+ try {
19216
19309
  if (!isVectorCompatibleWithCurrentIndex(embedding)) {
19217
19310
  await upgradeVectorSchemaAfterFirstEmbedding(embedding);
19218
19311
  }
@@ -19221,7 +19314,7 @@ async function backfillVectorEmbeddings(options2 = {}) {
19221
19314
  console.error(
19222
19315
  `[memorix] Backfill embedding mismatch for obs-${id}: provider returned ${embedding.length}d, index expects ${vectorDimensions ?? "unknown"}d (kept in queue)`
19223
19316
  );
19224
- lastFailure = `dimension mismatch: provider returned ${embedding.length}d, index expects ${vectorDimensions ?? "unknown"}d`;
19317
+ recordFailure(`dimension mismatch: provider returned ${embedding.length}d, index expects ${vectorDimensions ?? "unknown"}d`);
19225
19318
  failed++;
19226
19319
  continue;
19227
19320
  }
@@ -19260,15 +19353,10 @@ async function backfillVectorEmbeddings(options2 = {}) {
19260
19353
  await insertObservation(doc);
19261
19354
  vectorMissingIds.delete(id);
19262
19355
  succeeded++;
19263
- } else if (isEmbeddingExplicitlyDisabled()) {
19264
- vectorMissingIds.delete(id);
19265
- } else {
19266
- lastFailure = "embedding provider unavailable";
19356
+ } catch (error2) {
19357
+ recordFailure(vectorBackfillError(error2));
19267
19358
  failed++;
19268
19359
  }
19269
- } catch (err) {
19270
- lastFailure = err instanceof Error ? err.message : String(err);
19271
- failed++;
19272
19360
  }
19273
19361
  }
19274
19362
  } finally {
@@ -19281,7 +19369,12 @@ async function backfillVectorEmbeddings(options2 = {}) {
19281
19369
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
19282
19370
  };
19283
19371
  }
19284
- return { attempted: ids.length, succeeded, failed };
19372
+ return {
19373
+ attempted: ids.length,
19374
+ succeeded,
19375
+ failed,
19376
+ ...lastFailure ? { lastError: lastFailure } : {}
19377
+ };
19285
19378
  }
19286
19379
  var observations, nextId, projectDir, searchIndexPrepared, vectorMissingIds, vectorBackfillRunning, vectorSchemaUpgradePromise, lastVectorBackfill, embeddingFailureLogTimestamps, EMBEDDING_FAILURE_LOG_COOLDOWN_MS;
19287
19380
  var init_observations = __esm({
@@ -27569,6 +27662,7 @@ __export(setup_exports, {
27569
27662
  installOpenClawBundlePackage: () => installOpenClawBundlePackage,
27570
27663
  installPiPackage: () => installPiPackage,
27571
27664
  installPluginPackage: () => installPluginPackage,
27665
+ removeLegacyCodexMemorixMcpConfig: () => removeLegacyCodexMemorixMcpConfig,
27572
27666
  tryInstallCodexPlugin: () => tryInstallCodexPlugin
27573
27667
  });
27574
27668
  import { spawnSync } from "child_process";
@@ -27816,6 +27910,31 @@ function mergeTomlMcpConfig(existingContent, generatedContent) {
27816
27910
  return `${parts.join("\n\n")}
27817
27911
  `;
27818
27912
  }
27913
+ function isLegacyCodexMemorixNodeServer(server) {
27914
+ if (server.name !== "memorix" || server.command.trim().toLowerCase() !== "node") return false;
27915
+ const startsMemorixStdio = server.args.some((arg) => arg.trim().toLowerCase() === "serve");
27916
+ const sourceEntry = server.args.some((arg) => {
27917
+ const normalized = arg.replace(/\\/g, "/").toLowerCase();
27918
+ return /\/memorix\/dist\/cli\/index\.(?:[cm]?js)$/.test(normalized);
27919
+ });
27920
+ return startsMemorixStdio && sourceEntry;
27921
+ }
27922
+ async function removeLegacyCodexMemorixMcpConfig(configPath = path17.join(homedir24(), ".codex", "config.toml")) {
27923
+ let existingContent;
27924
+ try {
27925
+ existingContent = await readFile5(configPath, "utf-8");
27926
+ } catch {
27927
+ return { configPath, removed: false };
27928
+ }
27929
+ const adapter = new CodexMCPAdapter();
27930
+ const legacyServer = adapter.parse(existingContent).find(isLegacyCodexMemorixNodeServer);
27931
+ if (!legacyServer) return { configPath, removed: false };
27932
+ let nextContent = removeTomlSection(existingContent, "mcp_servers.memorix.env");
27933
+ nextContent = removeTomlSection(nextContent, "mcp_servers.memorix");
27934
+ await writeFile5(configPath, nextContent ? `${nextContent}
27935
+ ` : "", "utf-8");
27936
+ return { configPath, removed: true };
27937
+ }
27819
27938
  function mergeYamlMcpConfig(existingContent, generatedContent) {
27820
27939
  let existing = {};
27821
27940
  try {
@@ -28297,6 +28416,16 @@ async function installAgentSetup(agent, plan, global2) {
28297
28416
  const install = tryInstallCodexPlugin();
28298
28417
  if (install.ok) v3.success(install.message);
28299
28418
  else v3.warn(install.message);
28419
+ if (install.ok) {
28420
+ try {
28421
+ const migration = await removeLegacyCodexMemorixMcpConfig();
28422
+ if (migration.removed) {
28423
+ v3.info(`codex: removed legacy source-path MCP config from ${migration.configPath}`);
28424
+ }
28425
+ } catch {
28426
+ v3.warn("codex: plugin installed, but the legacy MCP config could not be checked");
28427
+ }
28428
+ }
28300
28429
  } else if (agent === "claude" && result.marketplaceRoot) {
28301
28430
  const install = tryInstallClaudePlugin(result.marketplaceRoot);
28302
28431
  if (install.ok) v3.success(install.message);
@@ -68531,6 +68660,26 @@ function vectorBatchSize(payload) {
68531
68660
  if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_VECTOR_BATCH_SIZE;
68532
68661
  return Math.min(100, Math.max(1, Math.floor(value)));
68533
68662
  }
68663
+ function vectorBackfillFailureStreak(payload) {
68664
+ const value = payload.vectorBackfillFailureStreak;
68665
+ return typeof value === "number" && Number.isFinite(value) ? Math.min(16, Math.max(0, Math.floor(value))) : 0;
68666
+ }
68667
+ function vectorBackfillRetryDelayMs(streak) {
68668
+ return Math.min(
68669
+ VECTOR_FAILURE_MAX_RETRY_DELAY_MS,
68670
+ VECTOR_FAILURE_BASE_RETRY_DELAY_MS * 2 ** Math.max(0, streak - 1)
68671
+ );
68672
+ }
68673
+ function withoutVectorBackfillFailureStreak(payload) {
68674
+ const { vectorBackfillFailureStreak: _streak, ...rest } = payload;
68675
+ return rest;
68676
+ }
68677
+ function resolveSupersededVectorFailures(projectDir2, projectId) {
68678
+ try {
68679
+ new MaintenanceJobStore(projectDir2).resolveFailedVectorBackfills(projectId);
68680
+ } catch {
68681
+ }
68682
+ }
68534
68683
  function retentionBatchSize(payload) {
68535
68684
  const value = payload.limit;
68536
68685
  if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_RETENTION_BATCH_SIZE;
@@ -68939,20 +69088,45 @@ function createProjectMaintenanceHandler(projectId, projectDir2, projectRoot, op
68939
69088
  if (isEmbeddingExplicitlyDisabled2()) return { action: "complete" };
68940
69089
  const { backfillVectorEmbeddings: backfillVectorEmbeddings2, getVectorStatus: getVectorStatus2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
68941
69090
  const before = getVectorStatus2(projectId);
68942
- if (before.missing === 0) return { action: "complete" };
69091
+ if (before.missing === 0) {
69092
+ resolveSupersededVectorFailures(projectDir2, projectId);
69093
+ return { action: "complete" };
69094
+ }
68943
69095
  const result = await backfillVectorEmbeddings2({
68944
69096
  projectId,
68945
69097
  limit: vectorBatchSize(job.payload)
68946
69098
  });
68947
69099
  const after = getVectorStatus2(projectId);
68948
- if (after.missing === 0) return { action: "complete" };
69100
+ if (after.missing === 0) {
69101
+ resolveSupersededVectorFailures(projectDir2, projectId);
69102
+ return { action: "complete" };
69103
+ }
68949
69104
  if (result.failed > 0 && result.succeeded === 0) {
68950
- throw new Error(`vector backfill made no progress (${result.failed}/${result.attempted} failed)`);
69105
+ const streak = vectorBackfillFailureStreak(job.payload) + 1;
69106
+ return {
69107
+ action: "reschedule",
69108
+ status: "retry",
69109
+ delayMs: vectorBackfillRetryDelayMs(streak),
69110
+ // This is a recoverable provider/index state, not a terminal job error.
69111
+ // Keeping one retry row prevents new MCP processes from creating a storm.
69112
+ resetAttempts: true,
69113
+ payload: {
69114
+ ...job.payload,
69115
+ vectorBackfillFailureStreak: streak
69116
+ },
69117
+ lastError: result.lastError ?? `vector backfill made no progress (${result.failed}/${result.attempted} failed)`
69118
+ };
68951
69119
  }
69120
+ const priorFailureStreak = vectorBackfillFailureStreak(job.payload);
69121
+ const payload = withoutVectorBackfillFailureStreak(job.payload);
68952
69122
  return {
68953
69123
  action: "reschedule",
68954
69124
  delayMs: result.attempted === 0 ? VECTOR_RETRY_DELAY_MS : 0,
68955
- resetAttempts: result.succeeded > 0
69125
+ resetAttempts: result.succeeded > 0,
69126
+ ...result.succeeded > 0 ? {
69127
+ ...priorFailureStreak > 0 ? { payload } : {},
69128
+ ...result.failed > 0 && result.lastError ? { lastError: result.lastError } : priorFailureStreak > 0 ? { clearLastError: true } : {}
69129
+ } : {}
68956
69130
  };
68957
69131
  };
68958
69132
  }
@@ -68965,11 +69139,12 @@ function createProjectMaintenanceDispatcher(projectId, projectDir2, projectRoot,
68965
69139
  return isolatedRunner({ job, projectRoot, dataDir: projectDir2 });
68966
69140
  };
68967
69141
  }
68968
- 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;
69142
+ 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;
68969
69143
  var init_project_maintenance = __esm({
68970
69144
  "src/runtime/project-maintenance.ts"() {
68971
69145
  "use strict";
68972
69146
  init_esm_shims();
69147
+ init_maintenance_jobs();
68973
69148
  init_isolated_maintenance();
68974
69149
  init_lifecycle();
68975
69150
  init_timeout();
@@ -68980,6 +69155,8 @@ var init_project_maintenance = __esm({
68980
69155
  DEFAULT_CLAIM_DERIVATION_BATCH_SIZE = 100;
68981
69156
  DEFAULT_OBSERVATION_QUALIFICATION_BATCH_SIZE = 100;
68982
69157
  VECTOR_RETRY_DELAY_MS = 5e3;
69158
+ VECTOR_FAILURE_BASE_RETRY_DELAY_MS = 6e4;
69159
+ VECTOR_FAILURE_MAX_RETRY_DELAY_MS = 15 * 6e4;
68983
69160
  DEDUP_PER_PAIR_TIMEOUT_MS = 5e3;
68984
69161
  }
68985
69162
  });
@@ -69862,6 +70039,17 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
69862
70039
  sourceCounts,
69863
70040
  recentObservations: sorted,
69864
70041
  embedding: embeddingStatus,
70042
+ // A standalone dashboard has no access to an active MCP
70043
+ // process's in-memory Orama index. Keep this explicit so the
70044
+ // UI never presents an empty local singleton as a healthy
70045
+ // all-vectors-indexed result.
70046
+ vectorStatus: {
70047
+ available: false,
70048
+ total: 0,
70049
+ missing: 0,
70050
+ missingIds: [],
70051
+ backfillRunning: false
70052
+ },
69865
70053
  storage: storageInfo,
69866
70054
  maintenance,
69867
70055
  ...lifecycle ? { lifecycle } : {},
@@ -71086,7 +71274,7 @@ The path should point to a directory containing a .git folder.`
71086
71274
  };
71087
71275
  const server = existingServer ?? new McpServer({
71088
71276
  name: "memorix",
71089
- version: true ? "1.2.7" : "1.0.1"
71277
+ version: true ? "1.2.8" : "1.0.1"
71090
71278
  });
71091
71279
  const originalRegisterTool = server.registerTool.bind(server);
71092
71280
  server.registerTool = ((name, ...args) => {
@@ -77462,10 +77650,18 @@ var init_serve_http = __esm({
77462
77650
  };
77463
77651
  } catch {
77464
77652
  }
77465
- let vectorStatus = { total: 0, missing: 0, missingIds: [], backfillRunning: false };
77653
+ let vectorStatus = {
77654
+ available: false,
77655
+ total: 0,
77656
+ missing: 0,
77657
+ missingIds: [],
77658
+ backfillRunning: false
77659
+ };
77466
77660
  try {
77467
- const { getVectorStatus: getVectorStatus2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
77468
- vectorStatus = getVectorStatus2(statsProjectId);
77661
+ const { getSearchIndexStatus: getSearchIndexStatus2, getVectorStatus: getVectorStatus2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
77662
+ if (getSearchIndexStatus2(statsProjectId).prepared) {
77663
+ vectorStatus = { available: true, ...getVectorStatus2(statsProjectId) };
77664
+ }
77469
77665
  } catch {
77470
77666
  }
77471
77667
  let searchMode = "fulltext";
@@ -82210,6 +82406,9 @@ function asRecordArray(value) {
82210
82406
  function codexPluginPath() {
82211
82407
  return `${homedir32()}/.codex/plugins/${CODEX_PLUGIN_NAME}`;
82212
82408
  }
82409
+ function codexPluginMcpPath() {
82410
+ return `${codexPluginPath()}/.mcp.json`;
82411
+ }
82213
82412
  function codexMarketplacePath() {
82214
82413
  return `${homedir32()}/.agents/plugins/marketplace.json`;
82215
82414
  }
@@ -82242,6 +82441,7 @@ async function inspectCodexPluginBundle() {
82242
82441
  const pluginPath = codexPluginPath();
82243
82442
  const manifestPath = `${pluginPath}/.codex-plugin/plugin.json`;
82244
82443
  const hooksPath = `${pluginPath}/hooks/hooks.json`;
82444
+ const mcpPath = codexPluginMcpPath();
82245
82445
  if (!existsSync25(pluginPath)) {
82246
82446
  return {
82247
82447
  scope: "global",
@@ -82289,6 +82489,19 @@ async function inspectCodexPluginBundle() {
82289
82489
  const issues = [];
82290
82490
  if (version2 !== getCliVersion()) issues.push("codex-plugin-version-mismatch");
82291
82491
  if (manifest.hooks !== "./hooks/hooks.json") issues.push("codex-hook-manifest-missing");
82492
+ if (!existsSync25(mcpPath)) {
82493
+ issues.push("codex-plugin-mcp-manifest-missing");
82494
+ } else {
82495
+ try {
82496
+ const mcpConfig = asRecord2(JSON.parse(await readFile7(mcpPath, "utf-8")));
82497
+ const server = coerceMcpServer("memorix", asRecord2(mcpConfig?.mcpServers)?.memorix);
82498
+ if (!server || !isRecommendedStdioServer(server)) {
82499
+ issues.push("codex-plugin-mcp-manifest-invalid");
82500
+ }
82501
+ } catch {
82502
+ issues.push("codex-plugin-mcp-manifest-unreadable");
82503
+ }
82504
+ }
82292
82505
  let declared = [];
82293
82506
  if (!existsSync25(hooksPath)) {
82294
82507
  issues.push("codex-hook-manifest-missing");
@@ -82438,7 +82651,9 @@ async function inspectCodexHookTrust() {
82438
82651
  kind: "hook-trust",
82439
82652
  path: configPath,
82440
82653
  exists: true,
82441
- status: issues.length > 0 ? "repairable" : "ok",
82654
+ // Codex records hook approval only after the user has reviewed it. Memorix
82655
+ // must not treat an intentional, host-owned consent step as a broken setup.
82656
+ status: issues.length > 0 ? "skipped" : "ok",
82442
82657
  issues,
82443
82658
  hookTrust: { trusted, expected: [...CODEX_HOOK_STATE_NAMES] }
82444
82659
  };
@@ -82506,7 +82721,7 @@ function defaultClaudeProjectKey(projectRoot) {
82506
82721
  function getClaudeLocalConfigPath() {
82507
82722
  return `${homedir32()}/.claude.json`;
82508
82723
  }
82509
- function coerceClaudeLocalServer(name, value) {
82724
+ function coerceMcpServer(name, value) {
82510
82725
  const entry = asRecord2(value);
82511
82726
  if (!entry) return null;
82512
82727
  const args = Array.isArray(entry.args) ? entry.args.map(String) : [];
@@ -82565,7 +82780,7 @@ async function inspectClaudeLocalMcp(projectRoot) {
82565
82780
  };
82566
82781
  }
82567
82782
  const servers = asRecord2(localProject.project.mcpServers);
82568
- const server = coerceClaudeLocalServer("memorix", servers?.memorix);
82783
+ const server = coerceMcpServer("memorix", servers?.memorix);
82569
82784
  if (!server) {
82570
82785
  return {
82571
82786
  scope: "local",
@@ -82616,6 +82831,56 @@ async function installClaudeLocalMcpConfig(projectRoot) {
82616
82831
  await writeFile7(configPath, `${JSON.stringify(config2, null, 2)}
82617
82832
  `, "utf-8");
82618
82833
  }
82834
+ async function inspectCodexPluginMcp() {
82835
+ const runtime = inspectCodexPluginRuntime();
82836
+ if (!runtime.runtime?.installed || !runtime.runtime.enabled) return null;
82837
+ const mcpPath = codexPluginMcpPath();
82838
+ if (!existsSync25(mcpPath)) {
82839
+ return {
82840
+ scope: "global",
82841
+ path: mcpPath,
82842
+ exists: false,
82843
+ status: "repairable",
82844
+ issues: ["codex-plugin-mcp-manifest-missing"],
82845
+ managedByPlugin: true
82846
+ };
82847
+ }
82848
+ try {
82849
+ const config2 = asRecord2(JSON.parse(await readFile7(mcpPath, "utf-8")));
82850
+ const server = coerceMcpServer("memorix", asRecord2(config2?.mcpServers)?.memorix);
82851
+ if (!server) {
82852
+ return {
82853
+ scope: "global",
82854
+ path: mcpPath,
82855
+ exists: true,
82856
+ status: "repairable",
82857
+ issues: ["codex-plugin-mcp-manifest-invalid"],
82858
+ managedByPlugin: true
82859
+ };
82860
+ }
82861
+ const issues = [];
82862
+ if (looksLikeStaleMemorixCommand(server)) issues.push("stale-command-path");
82863
+ if (!isRecommendedStdioServer(server)) issues.push("nonstandard-mcp-command");
82864
+ return {
82865
+ scope: "global",
82866
+ path: mcpPath,
82867
+ exists: true,
82868
+ status: issues.length > 0 ? "repairable" : "ok",
82869
+ issues,
82870
+ managedByPlugin: true,
82871
+ server: sanitizeServer(server)
82872
+ };
82873
+ } catch {
82874
+ return {
82875
+ scope: "global",
82876
+ path: mcpPath,
82877
+ exists: true,
82878
+ status: "repairable",
82879
+ issues: ["codex-plugin-mcp-manifest-unreadable"],
82880
+ managedByPlugin: true
82881
+ };
82882
+ }
82883
+ }
82619
82884
  async function inspectMcp(agent, projectRoot, scope) {
82620
82885
  if (!isMcpConfigAgent(agent)) {
82621
82886
  return { status: "skipped", issues: ["mcp-managed-by-package"], checks: [] };
@@ -82627,6 +82892,13 @@ async function inspectMcp(agent, projectRoot, scope) {
82627
82892
  checks.push(await inspectClaudeLocalMcp(projectRoot));
82628
82893
  continue;
82629
82894
  }
82895
+ if (agent === "codex" && targetScope === "global") {
82896
+ const pluginCheck = await inspectCodexPluginMcp();
82897
+ if (pluginCheck) {
82898
+ checks.push(pluginCheck);
82899
+ continue;
82900
+ }
82901
+ }
82630
82902
  const configPath = adapter.getConfigPath(targetScope === "project" ? projectRoot : void 0);
82631
82903
  if (targetScope === "global" && configPath === adapter.getConfigPath(projectRoot)) continue;
82632
82904
  if (!existsSync25(configPath)) {
@@ -82771,10 +83043,19 @@ function formatAgentIntegrationReport(report) {
82771
83043
  lines.push(`${entry.agent}: ${status}`);
82772
83044
  if (entry.mcp.issues.length > 0) lines.push(` MCP: ${entry.mcp.issues.join(", ")}`);
82773
83045
  if (entry.guidance.issues.length > 0) lines.push(` Guidance: ${entry.guidance.issues.join(", ")}`);
82774
- if (entry.plugin.issues.length > 0) lines.push(` Plugin: ${entry.plugin.issues.join(", ")}`);
83046
+ const hookTrustPending = entry.plugin.issues.includes("codex-hook-trust-pending");
83047
+ const pluginIssues = entry.plugin.issues.filter((issue2) => issue2 !== "codex-hook-trust-pending");
83048
+ if (pluginIssues.length > 0) lines.push(` Plugin: ${pluginIssues.join(", ")}`);
83049
+ if (hookTrustPending) {
83050
+ lines.push(" Hooks: waiting for Codex approval on first use; MCP remains available.");
83051
+ }
82775
83052
  }
82776
83053
  lines.push("");
82777
- lines.push(`Repair: ${report.repairCommand}`);
83054
+ if (report.summary.missing > 0 || report.summary.repairable > 0) {
83055
+ lines.push(`Repair: ${report.repairCommand}`);
83056
+ } else {
83057
+ lines.push("No file repair needed.");
83058
+ }
82778
83059
  return lines.join("\n");
82779
83060
  }
82780
83061
  async function repairAgentIntegrations(options2 = {}) {
@@ -82788,6 +83069,10 @@ async function repairAgentIntegrations(options2 = {}) {
82788
83069
  if (isMcpConfigAgent(entry.agent)) {
82789
83070
  for (const check2 of entry.mcp.checks) {
82790
83071
  if (check2.status === "ok" || check2.status === "skipped") continue;
83072
+ if (check2.managedByPlugin) {
83073
+ skipped.push(`${entry.agent}:mcp:${check2.scope}:plugin-managed`);
83074
+ continue;
83075
+ }
82791
83076
  if (check2.status === "missing" && !canInstallMissing) {
82792
83077
  skipped.push(`${entry.agent}:mcp:${check2.scope}:missing`);
82793
83078
  continue;