diff --git a/hermes_cli/cli_agent_setup_mixin.py b/hermes_cli/cli_agent_setup_mixin.py index 19d91cebb8..582b1e4b5e 100644 --- a/hermes_cli/cli_agent_setup_mixin.py +++ b/hermes_cli/cli_agent_setup_mixin.py @@ -144,9 +144,10 @@ class CLIAgentSetupMixin: api_key = runtime.get("api_key") base_url = runtime.get("base_url") resolved_provider = runtime.get("provider", "openrouter") - resolved_api_mode = runtime.get("api_mode", self.api_mode) - resolved_acp_command = runtime.get("command") - resolved_acp_args = list(runtime.get("args") or []) + resolved_routing = ( + resolved_provider, runtime.get("api_mode", self.api_mode), runtime.get("command"), + list(runtime.get("args") or []), + ) # A callable api_key is a bearer-token provider (Azure Entra ID): the OpenAI SDK # invokes it per request, so skip string validation / placeholder substitution. _is_callable_provider = callable(api_key) and not isinstance(api_key, str) @@ -174,16 +175,8 @@ class CLIAgentSetupMixin: return False credentials_changed = api_key != self.api_key or base_url != self.base_url - routing_changed = ( - resolved_provider != self.provider - or resolved_api_mode != self.api_mode - or resolved_acp_command != self.acp_command - or resolved_acp_args != self.acp_args - ) - self.provider = resolved_provider - self.api_mode = resolved_api_mode - self.acp_command = resolved_acp_command - self.acp_args = resolved_acp_args + routing_changed = resolved_routing != (self.provider, self.api_mode, self.acp_command, self.acp_args) + self.provider, self.api_mode, self.acp_command, self.acp_args = resolved_routing self._credential_pool = runtime.get("credential_pool") self._provider_source = runtime.get("source") self.api_key = api_key @@ -315,12 +308,9 @@ class CLIAgentSetupMixin: from hermes_cli.config import load_config _model_cfg = (load_config().get("model") or {}) if isinstance(_model_cfg, dict): - _new_provider = (_model_cfg.get("provider") or "").strip() - if _new_provider: - self.requested_provider = _new_provider + self.requested_provider = (_model_cfg.get("provider") or "").strip() or self.requested_provider _new_model = (_model_cfg.get("default") or _model_cfg.get("model") or "").strip() - if _new_model: - self.model = _new_model + self.model = _new_model or self.model except Exception as exc: logger.debug("first-run config re-sync failed: %s", exc) # Force credential re-resolution + agent rebuild on next use. @@ -466,7 +456,6 @@ class CLIAgentSetupMixin: self._session_db = SessionDB() except Exception as e: logger.warning("SQLite session store not available — session will NOT be indexed: %s", e) - if ( self._resumed and self._session_db and not self.conversation_history and not self._load_resumed_history_late() @@ -671,7 +660,6 @@ class CLIAgentSetupMixin: display_kind = msg.get("display_kind") content = msg.get("content") tool_calls = msg.get("tool_calls") or [] - if display_kind == "hidden": continue if display_kind in _RESUME_EVENT_TEXT: @@ -702,9 +690,8 @@ class CLIAgentSetupMixin: text = text[:MAX_ASST_LEN] + "..." parts.append(text) if tool_calls: - tc_summary = _tool_calls_summary(tool_calls) - parts.append(tc_summary) - full_parts.append(tc_summary) + parts.append(_tool_calls_summary(tool_calls)) + full_parts.append(parts[-1]) # Skip pure-reasoning messages with no visible output, and tool-call-only # entries when SKIP_TOOL_ONLY is enabled. if not text and (SKIP_TOOL_ONLY or not tool_calls): @@ -716,10 +703,8 @@ class CLIAgentSetupMixin: if not entries: return - skipped = 0 - if len(entries) > MAX_DISPLAY_EXCHANGES * 2: - skipped = len(entries) - MAX_DISPLAY_EXCHANGES * 2 - entries = entries[skipped:] + skipped = max(0, len(entries) - MAX_DISPLAY_EXCHANGES * 2) + entries = entries[skipped:] # Show the last assistant entry in full so the user sees where they left off. if _last_asst_idx is not None and _last_asst_full: diff --git a/hermes_cli/cli_terminal_mixin.py b/hermes_cli/cli_terminal_mixin.py index 7b1b87e8a5..ed522b418d 100644 --- a/hermes_cli/cli_terminal_mixin.py +++ b/hermes_cli/cli_terminal_mixin.py @@ -270,6 +270,21 @@ class CLITerminalMixin: original_on_resize() self._schedule_status_bar_unsuppress(app) + def _restart_debounce_timer(self, attr: str, delay: float, fn) -> None: + """Cancel the daemon Timer stored on ``self.`` (if any) and start a new one. + + ``fn`` receives the new Timer so it can detect being superseded.""" + old_timer = getattr(self, attr, None) + if old_timer is not None: + try: + old_timer.cancel() + except Exception: + pass + timer = threading.Timer(delay, lambda: fn(timer)) + timer.daemon = True + setattr(self, attr, timer) + timer.start() + def _schedule_status_bar_unsuppress(self, app, delay: float = 0.35) -> None: """Clear the post-resize status-bar suppression after the reflow settles. @@ -277,13 +292,6 @@ class CLITerminalMixin: the bar only once it stops. """ try: - old_timer = getattr(self, "_status_bar_unsuppress_timer", None) - if old_timer is not None: - try: - old_timer.cancel() - except Exception: - pass - def _clear(): self._status_bar_suppressed_after_resize = False try: @@ -291,10 +299,9 @@ class CLITerminalMixin: except Exception: pass - timer = threading.Timer(delay, lambda: _run_on_app_loop(app, _clear)) - timer.daemon = True - self._status_bar_unsuppress_timer = timer - timer.start() + self._restart_debounce_timer( + "_status_bar_unsuppress_timer", delay, lambda _t: _run_on_app_loop(app, _clear) + ) except Exception: # Fail open: never leave the bar stuck hidden. self._status_bar_suppressed_after_resize = False @@ -302,7 +309,6 @@ class CLITerminalMixin: def _schedule_resize_recovery(self, app, original_on_resize, delay: float = 0.12) -> None: """Debounce resize redraws so footer chrome is not stamped into scrollback.""" try: - old_timer = getattr(self, "_resize_recovery_timer", None) lock = getattr(self, "_resize_recovery_lock", None) if lock is None: lock = threading.Lock() @@ -312,7 +318,7 @@ class CLITerminalMixin: def _run_recovery(): with lock: if getattr(self, "_resize_recovery_timer", None) is not timer_ref: - return + return # superseded by a newer resize self._resize_recovery_timer = None self._resize_recovery_pending = False self._recover_after_resize(app, original_on_resize) @@ -320,16 +326,8 @@ class CLITerminalMixin: _run_on_app_loop(app, _run_recovery) with lock: - if old_timer is not None: - try: - old_timer.cancel() - except Exception: - pass self._resize_recovery_pending = True - timer = threading.Timer(delay, lambda: _timer_fired(timer)) - timer.daemon = True - self._resize_recovery_timer = timer - timer.start() + self._restart_debounce_timer("_resize_recovery_timer", delay, _timer_fired) except Exception: self._resize_recovery_pending = False self._recover_after_resize(app, original_on_resize) diff --git a/hermes_cli/codex_runtime_plugin_migration.py b/hermes_cli/codex_runtime_plugin_migration.py index 6e5f3829b2..66878cdcfc 100644 --- a/hermes_cli/codex_runtime_plugin_migration.py +++ b/hermes_cli/codex_runtime_plugin_migration.py @@ -170,8 +170,7 @@ def _quote_key(key: str) -> str: def render_codex_toml_section( - servers: dict[str, dict], - plugins: Optional[list[dict]] = None, + servers: dict[str, dict], plugins: Optional[list[dict]] = None, default_permission_profile: Optional[str] = None, ) -> str: """Render the managed [mcp_servers.] / [plugins.] / default_permissions block. @@ -288,8 +287,7 @@ def _strip_existing_managed_block(toml_text: str) -> str: def _query_codex_plugins( - codex_home: Optional[Path] = None, - timeout: float = 8.0, + codex_home: Optional[Path] = None, timeout: float = 8.0 ) -> tuple[list[dict], Optional[str]]: """Query codex's `plugin/list` for installed curated plugins. @@ -410,12 +408,8 @@ def _write_atomic(target: Path, text: str) -> None: def migrate( - hermes_config: dict, - *, - codex_home: Optional[Path] = None, - dry_run: bool = False, - discover_plugins: bool = True, - default_permission_profile: Optional[str] = ":workspace", + hermes_config: dict, *, codex_home: Optional[Path] = None, dry_run: bool = False, + discover_plugins: bool = True, default_permission_profile: Optional[str] = ":workspace", expose_hermes_tools: bool = True, ) -> MigrationReport: """Translate Hermes mcp_servers config + Codex curated plugins into ~/.codex/config.toml.