memorix 1.2.7 → 1.2.9

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 (38) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/cli/index.js +19531 -19017
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/dashboard/static/app.js +7 -2
  5. package/dist/index.js +7870 -7497
  6. package/dist/index.js.map +1 -1
  7. package/dist/maintenance-jobs-o1rYSFcM.d.ts +36 -0
  8. package/dist/maintenance-runner.d.ts +1 -28
  9. package/dist/maintenance-runner.js +5937 -5596
  10. package/dist/maintenance-runner.js.map +1 -1
  11. package/dist/memcode-runtime/CHANGELOG.md +15 -0
  12. package/dist/sdk.js +7866 -7493
  13. package/dist/sdk.js.map +1 -1
  14. package/dist/vector-backfill-runner.d.ts +31 -0
  15. package/dist/vector-backfill-runner.js +12670 -0
  16. package/dist/vector-backfill-runner.js.map +1 -0
  17. package/docs/dev-log/progress.txt +25 -0
  18. package/package.json +3 -3
  19. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  20. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  21. package/plugins/copilot/memorix/plugin.json +1 -1
  22. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  23. package/plugins/omp/memorix/package.json +1 -1
  24. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  25. package/plugins/pi/memorix/package.json +1 -1
  26. package/src/cli/commands/agent-integrations.ts +102 -5
  27. package/src/cli/commands/operator-shared.ts +4 -1
  28. package/src/cli/commands/serve-http.ts +58 -50
  29. package/src/cli/commands/setup.ts +44 -0
  30. package/src/dashboard/server.ts +54 -63
  31. package/src/memory/observations.ts +175 -99
  32. package/src/memory/retention.ts +106 -28
  33. package/src/memory/session.ts +71 -12
  34. package/src/runtime/maintenance-jobs.ts +84 -4
  35. package/src/runtime/project-maintenance.ts +63 -6
  36. package/src/runtime/vector-backfill-runner.ts +144 -0
  37. package/src/search/intent-detector.ts +39 -1
  38. package/src/server.ts +5 -5
@@ -339,3 +339,28 @@
339
339
  isolated Claude Code handoff/continuation smoke. `npm publish --dry-run
340
340
  --ignore-scripts` accepted the public 1.2.7 tarball; the full prepublish
341
341
  build/test path is covered by the root release gates above.
342
+
343
+ ## 1.2.8 Vector Recovery and Codex Installation Clarity
344
+ - Reworked vector backfill failure handling so transient provider or local
345
+ index failures leave one durable, diagnosable retry with bounded backoff
346
+ instead of consuming the maintenance retry budget in each new MCP process.
347
+ Healthy work clears stale diagnostics, revives an old failed vector job when
348
+ needed, and resolves superseded historical failure rows once recovery is
349
+ complete. Provider calls are batched while process-local Orama writes remain
350
+ serialized.
351
+ - Corrected dashboard and HTTP control-plane vector reporting. A process that
352
+ does not own a hydrated MCP search index now states that vector status belongs
353
+ to the active MCP session rather than showing a misleading green `0 / 0`.
354
+ - Corrected Codex install diagnostics: an enabled official Codex plugin owns
355
+ its `.mcp.json` `memorix serve` endpoint, while first-use hook approval is an
356
+ explicit host consent step rather than a broken installation. Plugin setup
357
+ removes only the legacy source-path `memorix` entry after successful plugin
358
+ installation; custom user-managed MCP entries are preserved.
359
+ - Release verification: focused regression tests (96 assertions), `npm run
360
+ lint`, production build, and the full root test suite (all passed, 280s).
361
+ A live OpenRouter `qwen/qwen3-embedding-8b` batch returned two 4096d vectors.
362
+ An isolated tarball install reported CLI version `1.2.8`; its stdio MCP
363
+ handshake exposed 44 tools and completed `memorix_project_context`, and its
364
+ CLI `context --json` path returned a structured Workset. `npm pack --dry-run`
365
+ and `npm publish --dry-run --ignore-scripts` both accepted the final tarball
366
+ without manifest normalization warnings.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "description": "Local-first shared memory layer for AI coding agents across MCP clients, Git history, reasoning context, and project sessions.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -11,8 +11,8 @@
11
11
  "packages/memcode"
12
12
  ],
13
13
  "bin": {
14
- "memorix": "./dist/cli/index.js",
15
- "memcode": "./dist/cli/memcode.js"
14
+ "memorix": "dist/cli/index.js",
15
+ "memcode": "dist/cli/memcode.js"
16
16
  },
17
17
  "main": "./dist/index.js",
18
18
  "types": "./dist/index.d.ts",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "memorix",
4
- "version": "1.2.7",
4
+ "version": "1.2.9",
5
5
  "description": "Shared workspace memory for Claude Code and other AI coding agents.",
6
6
  "author": {
7
7
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "description": "Shared workspace memory for Codex and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "description": "Shared local workspace memory for GitHub Copilot CLI and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "mcpServers": {
5
5
  "memorix": {
6
6
  "command": "memorix",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix-omp-package",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "description": "Memorix shared workspace memory package for Oh-my-Pi.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "description": "Shared workspace memory for OpenClaw and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix-pi-package",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "description": "Memorix shared workspace memory package for Pi coding agent.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -26,6 +26,8 @@ export interface AgentMcpCheck {
26
26
  exists: boolean;
27
27
  status: AgentIntegrationStatus;
28
28
  issues: string[];
29
+ /** This endpoint is supplied by an enabled Codex plugin, not config.toml. */
30
+ managedByPlugin?: boolean;
29
31
  server?: {
30
32
  transport: 'stdio' | 'http';
31
33
  command?: string;
@@ -229,6 +231,10 @@ function codexPluginPath(): string {
229
231
  return `${homedir()}/.codex/plugins/${CODEX_PLUGIN_NAME}`;
230
232
  }
231
233
 
234
+ function codexPluginMcpPath(): string {
235
+ return `${codexPluginPath()}/.mcp.json`;
236
+ }
237
+
232
238
  function codexMarketplacePath(): string {
233
239
  return `${homedir()}/.agents/plugins/marketplace.json`;
234
240
  }
@@ -267,6 +273,7 @@ async function inspectCodexPluginBundle(): Promise<AgentPluginCheck> {
267
273
  const pluginPath = codexPluginPath();
268
274
  const manifestPath = `${pluginPath}/.codex-plugin/plugin.json`;
269
275
  const hooksPath = `${pluginPath}/hooks/hooks.json`;
276
+ const mcpPath = codexPluginMcpPath();
270
277
  if (!existsSync(pluginPath)) {
271
278
  return {
272
279
  scope: 'global',
@@ -317,6 +324,20 @@ async function inspectCodexPluginBundle(): Promise<AgentPluginCheck> {
317
324
  if (version !== getCliVersion()) issues.push('codex-plugin-version-mismatch');
318
325
  if (manifest.hooks !== './hooks/hooks.json') issues.push('codex-hook-manifest-missing');
319
326
 
327
+ if (!existsSync(mcpPath)) {
328
+ issues.push('codex-plugin-mcp-manifest-missing');
329
+ } else {
330
+ try {
331
+ const mcpConfig = asRecord(JSON.parse(await readFile(mcpPath, 'utf-8')));
332
+ const server = coerceMcpServer('memorix', asRecord(mcpConfig?.mcpServers)?.memorix);
333
+ if (!server || !isRecommendedStdioServer(server)) {
334
+ issues.push('codex-plugin-mcp-manifest-invalid');
335
+ }
336
+ } catch {
337
+ issues.push('codex-plugin-mcp-manifest-unreadable');
338
+ }
339
+ }
340
+
320
341
  let declared: string[] = [];
321
342
  if (!existsSync(hooksPath)) {
322
343
  issues.push('codex-hook-manifest-missing');
@@ -479,7 +500,9 @@ async function inspectCodexHookTrust(): Promise<AgentPluginCheck> {
479
500
  kind: 'hook-trust',
480
501
  path: configPath,
481
502
  exists: true,
482
- status: issues.length > 0 ? 'repairable' : 'ok',
503
+ // Codex records hook approval only after the user has reviewed it. Memorix
504
+ // must not treat an intentional, host-owned consent step as a broken setup.
505
+ status: issues.length > 0 ? 'skipped' : 'ok',
483
506
  issues,
484
507
  hookTrust: { trusted, expected: [...CODEX_HOOK_STATE_NAMES] },
485
508
  };
@@ -562,7 +585,7 @@ function getClaudeLocalConfigPath(): string {
562
585
  return `${homedir()}/.claude.json`;
563
586
  }
564
587
 
565
- function coerceClaudeLocalServer(name: string, value: unknown): MCPServerEntry | null {
588
+ function coerceMcpServer(name: string, value: unknown): MCPServerEntry | null {
566
589
  const entry = asRecord(value);
567
590
  if (!entry) return null;
568
591
  const args = Array.isArray(entry.args) ? entry.args.map(String) : [];
@@ -626,7 +649,7 @@ async function inspectClaudeLocalMcp(projectRoot: string): Promise<AgentMcpCheck
626
649
  }
627
650
 
628
651
  const servers = asRecord(localProject.project.mcpServers);
629
- const server = coerceClaudeLocalServer('memorix', servers?.memorix);
652
+ const server = coerceMcpServer('memorix', servers?.memorix);
630
653
  if (!server) {
631
654
  return {
632
655
  scope: 'local',
@@ -683,6 +706,59 @@ async function installClaudeLocalMcpConfig(projectRoot: string): Promise<void> {
683
706
  await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
684
707
  }
685
708
 
709
+ async function inspectCodexPluginMcp(): Promise<AgentMcpCheck | null> {
710
+ const runtime = inspectCodexPluginRuntime();
711
+ if (!runtime.runtime?.installed || !runtime.runtime.enabled) return null;
712
+
713
+ const mcpPath = codexPluginMcpPath();
714
+ if (!existsSync(mcpPath)) {
715
+ return {
716
+ scope: 'global',
717
+ path: mcpPath,
718
+ exists: false,
719
+ status: 'repairable',
720
+ issues: ['codex-plugin-mcp-manifest-missing'],
721
+ managedByPlugin: true,
722
+ };
723
+ }
724
+
725
+ try {
726
+ const config = asRecord(JSON.parse(await readFile(mcpPath, 'utf-8')));
727
+ const server = coerceMcpServer('memorix', asRecord(config?.mcpServers)?.memorix);
728
+ if (!server) {
729
+ return {
730
+ scope: 'global',
731
+ path: mcpPath,
732
+ exists: true,
733
+ status: 'repairable',
734
+ issues: ['codex-plugin-mcp-manifest-invalid'],
735
+ managedByPlugin: true,
736
+ };
737
+ }
738
+ const issues: string[] = [];
739
+ if (looksLikeStaleMemorixCommand(server)) issues.push('stale-command-path');
740
+ if (!isRecommendedStdioServer(server)) issues.push('nonstandard-mcp-command');
741
+ return {
742
+ scope: 'global',
743
+ path: mcpPath,
744
+ exists: true,
745
+ status: issues.length > 0 ? 'repairable' : 'ok',
746
+ issues,
747
+ managedByPlugin: true,
748
+ server: sanitizeServer(server),
749
+ };
750
+ } catch {
751
+ return {
752
+ scope: 'global',
753
+ path: mcpPath,
754
+ exists: true,
755
+ status: 'repairable',
756
+ issues: ['codex-plugin-mcp-manifest-unreadable'],
757
+ managedByPlugin: true,
758
+ };
759
+ }
760
+ }
761
+
686
762
  async function inspectMcp(agent: AgentName, projectRoot: string, scope: AgentIntegrationScope): Promise<AgentIntegrationEntry['mcp']> {
687
763
  if (!isMcpConfigAgent(agent)) {
688
764
  return { status: 'skipped', issues: ['mcp-managed-by-package'], checks: [] };
@@ -697,6 +773,14 @@ async function inspectMcp(agent: AgentName, projectRoot: string, scope: AgentInt
697
773
  continue;
698
774
  }
699
775
 
776
+ if (agent === 'codex' && targetScope === 'global') {
777
+ const pluginCheck = await inspectCodexPluginMcp();
778
+ if (pluginCheck) {
779
+ checks.push(pluginCheck);
780
+ continue;
781
+ }
782
+ }
783
+
700
784
  const configPath = adapter.getConfigPath(targetScope === 'project' ? projectRoot : undefined);
701
785
  if (targetScope === 'global' && configPath === adapter.getConfigPath(projectRoot)) continue;
702
786
 
@@ -872,11 +956,20 @@ export function formatAgentIntegrationReport(report: AgentIntegrationReport): st
872
956
  lines.push(`${entry.agent}: ${status}`);
873
957
  if (entry.mcp.issues.length > 0) lines.push(` MCP: ${entry.mcp.issues.join(', ')}`);
874
958
  if (entry.guidance.issues.length > 0) lines.push(` Guidance: ${entry.guidance.issues.join(', ')}`);
875
- if (entry.plugin.issues.length > 0) lines.push(` Plugin: ${entry.plugin.issues.join(', ')}`);
959
+ const hookTrustPending = entry.plugin.issues.includes('codex-hook-trust-pending');
960
+ const pluginIssues = entry.plugin.issues.filter((issue) => issue !== 'codex-hook-trust-pending');
961
+ if (pluginIssues.length > 0) lines.push(` Plugin: ${pluginIssues.join(', ')}`);
962
+ if (hookTrustPending) {
963
+ lines.push(' Hooks: waiting for Codex approval on first use; MCP remains available.');
964
+ }
876
965
  }
877
966
 
878
967
  lines.push('');
879
- lines.push(`Repair: ${report.repairCommand}`);
968
+ if (report.summary.missing > 0 || report.summary.repairable > 0) {
969
+ lines.push(`Repair: ${report.repairCommand}`);
970
+ } else {
971
+ lines.push('No file repair needed.');
972
+ }
880
973
  return lines.join('\n');
881
974
  }
882
975
 
@@ -899,6 +992,10 @@ export async function repairAgentIntegrations(options: {
899
992
  if (isMcpConfigAgent(entry.agent)) {
900
993
  for (const check of entry.mcp.checks) {
901
994
  if (check.status === 'ok' || check.status === 'skipped') continue;
995
+ if (check.managedByPlugin) {
996
+ skipped.push(`${entry.agent}:mcp:${check.scope}:plugin-managed`);
997
+ continue;
998
+ }
902
999
  if (check.status === 'missing' && !canInstallMissing) {
903
1000
  skipped.push(`${entry.agent}:mcp:${check.scope}:missing`);
904
1001
  continue;
@@ -110,7 +110,10 @@ export async function getCliProjectContext(options?: CliContextOptions): Promise
110
110
  } catch {
111
111
  // CLI reads remain available even if optional background maintenance metadata fails.
112
112
  }
113
- await initObservations(dataDir);
113
+ await initObservations(dataDir, {
114
+ embeddingWriteMode: 'deferred',
115
+ projectRoot: project.rootPath,
116
+ });
114
117
  await initSessionStore(dataDir);
115
118
  const teamStore = await initTeamStore(dataDir);
116
119
 
@@ -24,6 +24,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
24
24
  import type { ObservationStore } from '../../store/obs-store.js';
25
25
  import { resolveToolProfile } from '../../server/tool-profile.js';
26
26
  import { scopeKnowledgeGraphToProject } from '../../memory/graph-scope.js';
27
+ import { projectObservationRetention, summarizeRetentionProjections } from '../../memory/retention.js';
27
28
  import { canManageObservation, filterReadableObservations } from '../../memory/visibility.js';
28
29
 
29
30
  export const DEFAULT_SESSION_TIMEOUT_MS = 30 * 60 * 1000;
@@ -760,20 +761,7 @@ export default defineCommand({
760
761
  await initGraphStore(statsDataDir);
761
762
  const graph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
762
763
 
763
- const observations = await loadDashboardProjectObservations(statsDataDir, statsProjectId, 'active') as Array<{
764
- type?: string;
765
- id?: number;
766
- title?: string;
767
- entityName?: string;
768
- createdAt?: string;
769
- source?: string;
770
- commitHash?: string;
771
- filesModified?: string[];
772
- relatedEntities?: string[];
773
- status?: string;
774
- importance?: number;
775
- accessCount?: number;
776
- }>;
764
+ const observations = await loadDashboardProjectObservations(statsDataDir, statsProjectId, 'active');
777
765
  const statsStore = await getDashboardObservationStore(statsDataDir);
778
766
  const nextId = await statsStore.loadIdCounter();
779
767
  const typeCounts: Record<string, number> = {};
@@ -811,20 +799,17 @@ export default defineCommand({
811
799
  filesModified: o.filesModified,
812
800
  }));
813
801
 
814
- let retentionSummary = { active: 0, stale: 0, archive: 0, immune: 0 };
815
- for (const obs of observations) {
816
- const age = now - new Date(obs.createdAt || now).getTime();
817
- const ageHours = age / (1000 * 60 * 60);
818
- const importance = obs.importance ?? 5;
819
- const accessCount = obs.accessCount ?? 0;
820
- const lambda = 0.01;
821
- const score = Math.min(importance * Math.exp(-lambda * ageHours) + Math.min(accessCount * 0.5, 3), 10);
822
- const isImmune = importance >= 8 || obs.type === 'gotcha' || obs.type === 'decision';
823
- if (isImmune) retentionSummary.immune++;
824
- if (score >= 3) retentionSummary.active++;
825
- else if (score >= 1) retentionSummary.stale++;
826
- else retentionSummary.archive++;
827
- }
802
+ const retention = summarizeRetentionProjections(
803
+ observations
804
+ .filter((observation) => observation.type !== 'probe')
805
+ .map((observation) => projectObservationRetention(observation, { referenceTime: new Date(now) })),
806
+ );
807
+ const retentionSummary = {
808
+ active: retention.active,
809
+ stale: retention.stale,
810
+ archive: retention.archiveCandidates,
811
+ immune: retention.immune,
812
+ };
828
813
 
829
814
  const sorted = [...observations].filter(o => o.type !== 'probe').sort((a, b) => (b.id || 0) - (a.id || 0)).slice(0, 10);
830
815
 
@@ -839,10 +824,21 @@ export default defineCommand({
839
824
  };
840
825
  } catch { /* best effort */ }
841
826
 
842
- let vectorStatus = { total: 0, missing: 0, missingIds: [] as number[], backfillRunning: false };
827
+ let vectorStatus = {
828
+ available: false,
829
+ total: 0,
830
+ missing: 0,
831
+ missingIds: [] as number[],
832
+ backfillRunning: false,
833
+ };
843
834
  try {
844
- const { getVectorStatus } = await import('../../memory/observations.js');
845
- vectorStatus = getVectorStatus(statsProjectId);
835
+ const { getSearchIndexStatus, getVectorStatus } = await import('../../memory/observations.js');
836
+ // The HTTP control plane has a different module graph from stdio MCP.
837
+ // Only report vector counts when this process actually owns a hydrated
838
+ // index; an empty singleton must not look like a healthy 0/0 state.
839
+ if (getSearchIndexStatus(statsProjectId).prepared) {
840
+ vectorStatus = { available: true, ...getVectorStatus(statsProjectId) };
841
+ }
846
842
  } catch { /* best effort */ }
847
843
 
848
844
  // Real search mode from the last actual search execution
@@ -948,26 +944,38 @@ export default defineCommand({
948
944
 
949
945
  if (apiPath === '/retention') {
950
946
  const { projectId: retProjectId, dataDir: retDataDir } = await resolveRequestProject(url);
951
- const observations = await loadDashboardProjectObservations(retDataDir, retProjectId, 'active') as Array<{ id?: number; title?: string; type?: string; importance?: number; accessCount?: number; lastAccessedAt?: string; createdAt?: string; entityName?: string }>;
952
- const now = Date.now();
953
- const scored = observations.map(obs => {
954
- const age = now - new Date(obs.createdAt || now).getTime();
955
- const ageHours = age / (1000 * 60 * 60);
956
- const importance = obs.importance ?? 5;
957
- const accessCount = obs.accessCount ?? 0;
958
- const lambda = 0.01;
959
- const decayScore = importance * Math.exp(-lambda * ageHours);
960
- const accessBonus = Math.min(accessCount * 0.5, 3);
961
- const score = Math.min(decayScore + accessBonus, 10);
962
- const isImmune = importance >= 8 || obs.type === 'gotcha' || obs.type === 'decision';
963
- return { id: obs.id, title: obs.title, type: obs.type, entityName: obs.entityName, score: Math.round(score * 100) / 100, isImmune, ageHours: Math.round(ageHours * 10) / 10, accessCount };
947
+ const observations = await loadDashboardProjectObservations(retDataDir, retProjectId, 'active');
948
+ const referenceTime = new Date();
949
+ const rows = observations
950
+ .filter((observation) => observation.type !== 'probe')
951
+ .map((observation) => ({
952
+ observation,
953
+ retention: projectObservationRetention(observation, { referenceTime }),
954
+ }))
955
+ .sort((a, b) => b.retention.displayScore - a.retention.displayScore);
956
+ const summary = summarizeRetentionProjections(rows.map((row) => row.retention));
957
+ const items = rows.map(({ observation, retention }) => ({
958
+ id: observation.id,
959
+ title: observation.title,
960
+ type: observation.type,
961
+ entityName: observation.entityName,
962
+ score: retention.displayScore,
963
+ isImmune: retention.immune,
964
+ zone: retention.zone,
965
+ ageHours: retention.ageHours,
966
+ accessCount: retention.accessCount,
967
+ effectiveRetentionDays: retention.effectiveRetentionDays,
968
+ immunityReason: retention.immunityReason,
969
+ }));
970
+ sendJson({
971
+ summary: {
972
+ active: summary.active,
973
+ stale: summary.stale,
974
+ archive: summary.archiveCandidates,
975
+ immune: summary.immune,
976
+ },
977
+ items,
964
978
  });
965
- scored.sort((a, b) => b.score - a.score);
966
- const activeCount = scored.filter(s => s.score >= 3).length;
967
- const staleCount = scored.filter(s => s.score < 3 && s.score >= 1).length;
968
- const archiveCount = scored.filter(s => s.score < 1).length;
969
- const immuneCount = scored.filter(s => s.isImmune).length;
970
- sendJson({ summary: { active: activeCount, stale: staleCount, archive: archiveCount, immune: immuneCount }, items: scored });
971
979
  return;
972
980
  }
973
981
 
@@ -484,6 +484,40 @@ function mergeTomlMcpConfig(existingContent: string | null, generatedContent: st
484
484
  return `${parts.join('\n\n')}\n`;
485
485
  }
486
486
 
487
+ function isLegacyCodexMemorixNodeServer(server: MCPServerEntry): boolean {
488
+ if (server.name !== 'memorix' || server.command.trim().toLowerCase() !== 'node') return false;
489
+ const startsMemorixStdio = server.args.some((arg) => arg.trim().toLowerCase() === 'serve');
490
+ const sourceEntry = server.args.some((arg) => {
491
+ const normalized = arg.replace(/\\/g, '/').toLowerCase();
492
+ return /\/memorix\/dist\/cli\/index\.(?:[cm]?js)$/.test(normalized);
493
+ });
494
+ return startsMemorixStdio && sourceEntry;
495
+ }
496
+
497
+ /**
498
+ * Codex plugins now own Memorix stdio MCP registration. Remove only the old
499
+ * source-path form emitted by pre-plugin setup, never a user's custom server.
500
+ */
501
+ export async function removeLegacyCodexMemorixMcpConfig(
502
+ configPath = path.join(homedir(), '.codex', 'config.toml'),
503
+ ): Promise<{ configPath: string; removed: boolean }> {
504
+ let existingContent: string;
505
+ try {
506
+ existingContent = await readFile(configPath, 'utf-8');
507
+ } catch {
508
+ return { configPath, removed: false };
509
+ }
510
+
511
+ const adapter = new CodexMCPAdapter();
512
+ const legacyServer = adapter.parse(existingContent).find(isLegacyCodexMemorixNodeServer);
513
+ if (!legacyServer) return { configPath, removed: false };
514
+
515
+ let nextContent = removeTomlSection(existingContent, 'mcp_servers.memorix.env');
516
+ nextContent = removeTomlSection(nextContent, 'mcp_servers.memorix');
517
+ await writeFile(configPath, nextContent ? `${nextContent}\n` : '', 'utf-8');
518
+ return { configPath, removed: true };
519
+ }
520
+
487
521
  function mergeYamlMcpConfig(existingContent: string | null, generatedContent: string): string {
488
522
  let existing: Record<string, unknown> = {};
489
523
  try {
@@ -1091,6 +1125,16 @@ export async function installAgentSetup(agent: AgentName, plan: SetupPlan, globa
1091
1125
  const install = tryInstallCodexPlugin();
1092
1126
  if (install.ok) p.log.success(install.message);
1093
1127
  else p.log.warn(install.message);
1128
+ if (install.ok) {
1129
+ try {
1130
+ const migration = await removeLegacyCodexMemorixMcpConfig();
1131
+ if (migration.removed) {
1132
+ p.log.info(`codex: removed legacy source-path MCP config from ${migration.configPath}`);
1133
+ }
1134
+ } catch {
1135
+ p.log.warn('codex: plugin installed, but the legacy MCP config could not be checked');
1136
+ }
1137
+ }
1094
1138
  } else if (agent === 'claude' && result.marketplaceRoot) {
1095
1139
  const install = tryInstallClaudePlugin(result.marketplaceRoot);
1096
1140
  if (install.ok) p.log.success(install.message);