fix(cli): release the old agent's clients before dropping it for a rebuild
Interactive CLI sites that set self.agent = None (/personality, /reasoning, /fast, model/route/credential change, first-run setup, MoA one-shot) now go through _retire_agent(), which calls agent.release_clients() first: on the codex_app_server route the app-server child belongs to the instance and was orphaned for the CLI process lifetime (#72548).
This commit is contained in:
@@ -212,6 +212,16 @@ def _resume_panel_colors() -> tuple:
|
||||
return tuple(default for _, default in _RESUME_SKIN_COLORS)
|
||||
|
||||
|
||||
def _retire_agent(cli) -> None:
|
||||
"""Drop ``cli.agent`` so the next turn rebuilds it, releasing its LLM clients first: the Codex
|
||||
app-server child (and MCP descendants) belongs to the instance, so ``self.agent = None`` alone
|
||||
orphans it for the CLI process lifetime (#72548). Session tool state is kept (soft release)."""
|
||||
agent = cli.agent
|
||||
if agent is not None and hasattr(agent, "release_clients"):
|
||||
agent.release_clients()
|
||||
cli.agent = None
|
||||
|
||||
|
||||
class CLIAgentSetupMixin:
|
||||
"""Agent construction + session-resume display methods for ``HermesCLI``."""
|
||||
|
||||
@@ -324,7 +334,7 @@ class CLIAgentSetupMixin:
|
||||
|
||||
# AIAgent/OpenAI client holds auth at init, so rebuild on key/routing/model change.
|
||||
if (credentials_changed or routing_changed or model_changed) and self.agent is not None:
|
||||
self.agent = None
|
||||
_retire_agent(self)
|
||||
self._active_agent_route_signature = None
|
||||
return True
|
||||
|
||||
@@ -492,7 +502,7 @@ class CLIAgentSetupMixin:
|
||||
except Exception as exc:
|
||||
logger.debug("first-run config re-sync failed: %s", exc)
|
||||
# Force credential re-resolution + agent rebuild on next use.
|
||||
self.agent = None
|
||||
_retire_agent(self)
|
||||
self._active_agent_route_signature = None
|
||||
if self._runtime_credentials_ready():
|
||||
_cprint(" ✓ Provider configured — you're ready to chat.")
|
||||
|
||||
@@ -18,6 +18,8 @@ from rich import box as rich_box
|
||||
from rich.panel import Panel
|
||||
from typing import Optional
|
||||
|
||||
from hermes_cli.cli_agent_setup_mixin import _retire_agent
|
||||
|
||||
|
||||
class CLIChatTurnMixin:
|
||||
"""chat() and its per-turn phase helpers."""
|
||||
@@ -51,7 +53,7 @@ class CLIChatTurnMixin:
|
||||
|
||||
turn_route = self._resolve_turn_agent_config(message)
|
||||
if turn_route["signature"] != self._active_agent_route_signature:
|
||||
self.agent = None
|
||||
_retire_agent(self)
|
||||
if self.agent is None:
|
||||
_cprint(f"{_DIM}Initializing agent...{_RST}")
|
||||
if not self._init_agent(model_override=turn_route["model"], runtime_override=turn_route["runtime"],
|
||||
@@ -341,7 +343,7 @@ class CLIChatTurnMixin:
|
||||
for _key, _value in _restore.items():
|
||||
if _value is not None:
|
||||
setattr(self, _key, _value)
|
||||
self.agent = None
|
||||
_retire_agent(self)
|
||||
self._pending_moa_restore_model = None
|
||||
self._pending_moa_disable_after_turn = False
|
||||
except Exception as exc:
|
||||
|
||||
@@ -31,6 +31,7 @@ from rich.panel import Panel
|
||||
from hermes_constants import display_hermes_home, is_termux as _is_termux_environment
|
||||
from hermes_state_ids import new_session_id as mint_session_id
|
||||
from agent.turn_context import extract_api_content_sidecar
|
||||
from hermes_cli.cli_agent_setup_mixin import _retire_agent
|
||||
from hermes_cli.browser_connect import (
|
||||
DEFAULT_BROWSER_CDP_URL, discover_local_cdp_url, find_free_debug_port, is_browser_debug_ready,
|
||||
launch_chrome_debug, local_port_in_use, manual_chrome_debug_command)
|
||||
@@ -1555,12 +1556,12 @@ class CLICommandsMixin:
|
||||
cfg_get(read_raw_config(), "agent", "system_prompt", default=""))
|
||||
except Exception:
|
||||
self.system_prompt = ""
|
||||
self.agent = None # Force re-init
|
||||
_retire_agent(self) # Force re-init
|
||||
_pr(f"{face} Personality cleared {scope}",
|
||||
" No personality overlay — using base agent behavior.")
|
||||
else:
|
||||
self.system_prompt = personality_prompt
|
||||
self.agent = None # Force re-init
|
||||
_retire_agent(self) # Force re-init
|
||||
_pr(f"{face} Personality set to '{name}' {scope}",
|
||||
f" \"{_ellipsize(personality_prompt, 60)}\"")
|
||||
|
||||
@@ -2595,7 +2596,7 @@ class CLICommandsMixin:
|
||||
_dim_line('Display: show, hide'),
|
||||
_dim_line('Scope: session-scoped by default, --global to persist'))
|
||||
self.reasoning_config = parsed
|
||||
self.agent = None # Force agent re-init with new reasoning config
|
||||
_retire_agent(self) # Force agent re-init with new reasoning config
|
||||
saved = explicit_global and _save("agent.reasoning_effort", arg)
|
||||
if saved:
|
||||
if not isinstance(CLI_CONFIG.get("agent"), dict):
|
||||
@@ -2652,7 +2653,7 @@ class CLICommandsMixin:
|
||||
if arg not in _FAST_TIERS:
|
||||
return _cp(_dim_line(f'(._.) Unknown argument: {arg}'), usage)
|
||||
self.service_tier, saved_value = _FAST_TIERS[arg]
|
||||
self.agent = None # Force agent re-init with new service-tier config
|
||||
_retire_agent(self) # Force agent re-init with new service-tier config
|
||||
saved = explicit_global and _save("agent.service_tier", saved_value)
|
||||
outcome = _scope_outcome(explicit_global, saved)
|
||||
_cp(_accent_line(f"✓ {feature_name} set to {saved_value.upper()} {outcome}"))
|
||||
|
||||
@@ -16,6 +16,7 @@ import threading
|
||||
|
||||
from rich.markup import escape as _escape
|
||||
from utils import base_url_host_matches
|
||||
from hermes_cli.cli_agent_setup_mixin import _retire_agent
|
||||
|
||||
# CLI-level fields describing the active model route; snapshotted before a switch / one-turn
|
||||
# override and restored wholesale on rollback. ``reasoning_config`` rides along because it is
|
||||
@@ -910,7 +911,7 @@ class CLIModelSwitchMixin:
|
||||
self.api_key = "moa-virtual-provider"
|
||||
self.base_url = "moa://local"
|
||||
self.api_mode = "chat_completions"
|
||||
self.agent = None
|
||||
_retire_agent(self)
|
||||
self._pending_moa_disable_after_turn = True
|
||||
self._pending_agent_seed = payload
|
||||
_cprint(f" MoA one-shot queued with preset {preset}; previous model will be restored after this turn.")
|
||||
|
||||
27
tests/hermes_cli/test_cli_retire_agent.py
Normal file
27
tests/hermes_cli/test_cli_retire_agent.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""The interactive CLI must release the old AIAgent's LLM clients when it drops the instance
|
||||
for a rebuild (/personality, /reasoning, /fast, model/route/credential change, MoA one-shot):
|
||||
on the codex_app_server route the app-server child belongs to that instance and ``self.agent =
|
||||
None`` alone orphans it for the CLI process lifetime (#72548)."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from hermes_cli.cli_commands_mixin import CLICommandsMixin
|
||||
|
||||
|
||||
class _FakeAgent:
|
||||
def __init__(self):
|
||||
self.release_calls = 0
|
||||
|
||||
def release_clients(self):
|
||||
self.release_calls += 1
|
||||
|
||||
|
||||
def test_reasoning_command_releases_old_agent_clients_before_rebuild():
|
||||
agent = _FakeAgent()
|
||||
stub = SimpleNamespace(reasoning_config={"enabled": True, "effort": "medium"}, show_reasoning=False, agent=agent)
|
||||
with patch("cli.save_config_value"), patch("cli._cprint"):
|
||||
CLICommandsMixin._handle_reasoning_command(stub, "/reasoning high")
|
||||
assert stub.reasoning_config == {"enabled": True, "effort": "high"}
|
||||
assert stub.agent is None
|
||||
assert agent.release_calls == 1
|
||||
Reference in New Issue
Block a user