{"repo":"NousResearch/hermes-agent","public_id":"26529","display_label":"codex-runtime migration skipped when already enabled","summary":"A bug where re-running the /codex-runtime codex_app_server command silently skips migration if openai_runtime is already set in config, along with associated fix attempts and salvage PRs addressing that behavior.","subsystem":"agent","counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":56563,"merged_at":"2026-07-01T18:21:55+00:00"},"resolution":{"state":"resolved","by":"pr-56563"},"freshness":{"state":"verified","last_verified_at":"2026-07-01T18:21:55+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":26529,"pr_number":56563,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-01T18:21:55+00:00"}],"members":[{"kind":"issue","number":26529,"title":"/codex-runtime codex_app_server silently skips migration when openai_runtime already set in config","url":null,"state":"closed"},{"kind":"pr","number":26532,"title":"fix(codex-runtime): re-running /codex-runtime codex_app_server when already enabled now triggers migration","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"35eb93c8d","number":0},{"kind":"commit","value":"7322da487","number":0},{"kind":"pr","value":"#56563","number":56563}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":26535,"title":"fix(codex-runtime): reapply migration when already enabled","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#26532","number":26532}],"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":56563,"title":"fix(codex-runtime): re-running /codex-runtime codex_app_server when already enabled now triggers migration (salvage of #26532 by @simpolism, closes #26529)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/codex_runtime_switch.py","tests/hermes_cli/test_codex_runtime_switch.py"],"fix_hunks":[{"file":"hermes_cli/codex_runtime_switch.py","hunks":["@@ -144,8 +144,17 @@ def _check_binary_cached() -> tuple[bool, Optional[str]]:\n             codex_version=ver if ok else None,\n         )\n \n-    # No change requested\n-    if new_value == current:\n+    # No-config-change paths. For `auto` we return immediately — disabling\n+    # doesn't touch ~/.codex/. For `codex_app_server`, we fall through to\n+    # the migration block below: the config value is already correct, but\n+    # the world state (managed block in ~/.codex/config.toml, hermes-tools\n+    # MCP callback, plugin discovery) may be stale or missing — common\n+    # footgun when users pre-set `openai_runtime: codex_app_server` in\n+    # config.yaml without ever running the slash command. The migration is\n+    # idempotent by design (it replaces its own managed block in place), so\n+    # re-running is cheap and safe.\n+    reapplying_enable = new_value == current == \"codex_app_server\"\n+    if new_value == current and not reapplying_enable:\n         return CodexRuntimeStatus(\n             success=True,\n             new_value=current,","@@ -172,22 +181,26 @@ def _check_binary_cached() -> tuple[bool, Optional[str]]:\n                 codex_version=None,\n             )\n \n-    set_runtime(config, new_value)\n-    if persist_callback is not None:\n-        try:\n-            persist_callback(config)\n-        except Exception as exc:\n-            logger.exception(\"failed to persist openai_runtime change\")\n-            return CodexRuntimeStatus(\n-                success=False,\n-                new_value=new_value,\n-                old_value=current,\n-                message=f\"updated config in memory but persist failed: {exc}\",\n-            )\n+    if not reapplying_enable:\n+        set_runtime(config, new_value)\n+        if persist_callback is not None:\n+            try:\n+                persist_callback(config)\n+            except Exception as exc:\n+                logger.exception(\"failed to persist openai_runtime change\")\n+                return CodexRuntimeStatus(\n+                    success=False,\n+                    new_value=new_value,\n+                    old_value=current,\n+                    message=f\"updated config in memory but persist failed: {exc}\",\n+                )\n \n-    msg_lines = [\n-        f\"openai_runtime: {current} → {new_value}\",\n-    ]\n+    if reapplying_enable:\n+        msg_lines = [\n+            f\"openai_runtime already set to {current} — re-applying migration\"\n+        ]\n+    else:\n+        msg_lines = [f\"openai_runtime: {current} → {new_value}\"]\n     if new_value == \"codex_app_server\":\n         ok, ver = _check_binary_cached()\n         if ok:"]},{"file":"tests/hermes_cli/test_codex_runtime_switch.py","hunks":["@@ -96,6 +96,56 @@ def test_no_change_when_already_set(self):\n         assert r.success\n         assert r.message == \"openai_runtime already set to auto\"\n \n+    def test_reapply_codex_app_server_runs_migration(self):\n+        \"\"\"Re-applying codex_app_server when already enabled must still\n+        run the migration. Common footgun: user pre-sets\n+        `openai_runtime: codex_app_server` in config.yaml, then runs\n+        /codex-runtime codex_app_server expecting the migration. Without\n+        this, the slash command short-circuits with \"already set\" and\n+        ~/.codex/config.toml never gets the hermes-tools MCP callback\n+        or plugin migration — silent partial setup.\n+        \"\"\"\n+        cfg = {\n+            \"model\": {\"openai_runtime\": \"codex_app_server\"},\n+            \"mcp_servers\": {\n+                \"filesystem\": {\"command\": \"npx\", \"args\": [\"-y\", \"fs-server\"]},\n+            },\n+        }\n+        persisted = {}\n+\n+        def persist(c):\n+            persisted.update(c)\n+\n+        with patch.object(crs, \"check_codex_binary_ok\",\n+                          return_value=(True, \"0.130.0\")), \\\n+             patch(\"hermes_cli.codex_runtime_plugin_migration.migrate\") as mig:\n+            mig.return_value.migrated = [\"filesystem\", \"hermes-tools\"]\n+            mig.return_value.migrated_plugins = []\n+            mig.return_value.plugin_query_error = None\n+            mig.return_value.wrote_permissions_default = \":workspace\"\n+            mig.return_value.errors = []\n+            mig.return_value.target_path = \"/fake/.codex/config.toml\"\n+            r = crs.apply(cfg, \"codex_app_server\",\n+                          persist_callback=persist)\n+        assert r.success\n+        assert mig.called, \"migration must run on reapply, not just first enable\"\n+        # Re-apply should signal \"already set\" but still announce migration ran\n+        assert \"already set\" in r.message\n+        assert \"re-applying migration\" in r.message\n+        # Migration output still surfaces\n+        assert \"Migrated 1 MCP server\" in r.message\n+        assert \"filesystem\" in r.message\n+        assert \"Default sandbox: :workspace\" in r.message\n+        # No config write needed when value is unchanged — the persist\n+        # callback should NOT have fired (avoids spurious config.yaml mtimes\n+        # on every re-apply).\n+        assert persisted == {}, (\n+            \"persist_callback fired despite no config-value change\"\n+        )\n+        # Caller still needs a fresh session for the cached agent to pick\n+        # up any migration-driven changes.\n+        assert r.requires_new_session is True\n+\n     def test_enable_blocked_when_codex_missing(self):\n         cfg = {}\n         with patch.object(crs, \"check_codex_binary_ok\",\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}