From e3940f980799c85b0d37d0dd5c444de33f221717 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Fri, 24 Apr 2026 12:57:51 -0500 Subject: [PATCH] fix(tui): guard personality overlay when personalities is null TUI auto-resolves `display.personality` at session init, unlike the base CLI. If config contains `agent.personalities: null`, `_resolve_personality_prompt` called `.get()` on None and failed before model/provider selection. Normalize null personalities to `{}` and surface a targeted config warning. --- tests/tui_gateway/test_make_agent_provider.py | 50 ++++++++++++++++++- tui_gateway/server.py | 39 +++++++++++---- 2 files changed, 77 insertions(+), 12 deletions(-) diff --git a/tests/tui_gateway/test_make_agent_provider.py b/tests/tui_gateway/test_make_agent_provider.py index 7ad900d0d..9fc78882e 100644 --- a/tests/tui_gateway/test_make_agent_provider.py +++ b/tests/tui_gateway/test_make_agent_provider.py @@ -67,11 +67,25 @@ def test_probe_config_health_flags_null_sections(): assert "model" not in msg +def test_probe_config_health_flags_null_personalities_with_active_personality(): + from tui_gateway.server import _probe_config_health + + msg = _probe_config_health( + { + "agent": {"personalities": None}, + "display": {"personality": "kawaii"}, + "model": {}, + } + ) + assert "display.personality" in msg + assert "agent.personalities" in msg + + def test_make_agent_tolerates_null_config_sections(): """Bare `agent:` / `display:` keys in ~/.hermes/config.yaml parse as None. cfg.get("agent", {}) returns None (default only fires on missing key), so downstream .get() chains must be guarded. Reported via Twitter - against the new TUI; CLI path is unaffected.""" + against the new TUI.""" fake_runtime = { "provider": "openrouter", @@ -99,3 +113,37 @@ def test_make_agent_tolerates_null_config_sections(): _make_agent("sid-null", "key-null") assert mock_agent.called + + +def test_make_agent_tolerates_null_personalities_with_active_personality(): + fake_runtime = { + "provider": "openrouter", + "base_url": "https://api.synthetic.new/v1", + "api_key": "sk-test", + "api_mode": "chat_completions", + "command": None, + "args": None, + "credential_pool": None, + } + cfg = { + "agent": {"personalities": None}, + "display": {"personality": "kawaii"}, + "model": {"default": "glm-5"}, + } + + with ( + patch("tui_gateway.server._load_cfg", return_value=cfg), + patch("tui_gateway.server._get_db", return_value=MagicMock()), + patch("cli.load_cli_config", return_value={"agent": {"personalities": None}}), + patch( + "hermes_cli.runtime_provider.resolve_runtime_provider", + return_value=fake_runtime, + ), + patch("run_agent.AIAgent") as mock_agent, + ): + from tui_gateway.server import _make_agent + + _make_agent("sid-null-personality", "key-null-personality") + + assert mock_agent.called + assert mock_agent.call_args.kwargs["ephemeral_system_prompt"] is None diff --git a/tui_gateway/server.py b/tui_gateway/server.py index a854a84f2..e919c7c98 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -829,15 +829,32 @@ def _probe_config_health(cfg: dict) -> str: drop nested settings. Returns warning or ''.""" if not isinstance(cfg, dict): return "" + warnings: list[str] = [] null_keys = sorted(k for k, v in cfg.items() if v is None) if not null_keys: - return "" - keys = ", ".join(f"`{k}`" for k in null_keys) - return ( - f"config.yaml has empty section(s): {keys}. " - f"Remove the line(s) or set them to `{{}}` — " - f"empty sections silently drop nested settings." - ) + pass + else: + keys = ", ".join(f"`{k}`" for k in null_keys) + warnings.append( + f"config.yaml has empty section(s): {keys}. " + f"Remove the line(s) or set them to `{{}}` — " + f"empty sections silently drop nested settings." + ) + display_cfg = cfg.get("display") + agent_cfg = cfg.get("agent") + if isinstance(display_cfg, dict): + personality = str(display_cfg.get("personality", "") or "").strip().lower() + if ( + personality + and personality not in {"default", "none", "neutral"} + and isinstance(agent_cfg, dict) + and agent_cfg.get("personalities") is None + ): + warnings.append( + "`display.personality` is set but `agent.personalities` is empty/null; " + "personality overlay will be skipped." + ) + return " ".join(warnings).strip() def _session_info(agent) -> dict: @@ -1134,16 +1151,16 @@ def _resolve_personality_prompt(cfg: dict) -> str: try: from cli import load_cli_config - personalities = (load_cli_config().get("agent") or {}).get("personalities", {}) + personalities = (load_cli_config().get("agent") or {}).get("personalities", {}) or {} except Exception: try: from hermes_cli.config import load_config as _load_full_cfg - personalities = (_load_full_cfg().get("agent") or {}).get( - "personalities", {} + personalities = ( + (_load_full_cfg().get("agent") or {}).get("personalities", {}) or {} ) except Exception: - personalities = (cfg.get("agent") or {}).get("personalities", {}) + personalities = (cfg.get("agent") or {}).get("personalities", {}) or {} pval = personalities.get(name) if pval is None: return ""