diff --git a/gateway/run.py b/gateway/run.py index 1728d58b1b..de80262710 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -8331,6 +8331,27 @@ class GatewayRunner: # ──────────────────────────────────────────────────────────────── # /goal — persistent cross-turn goals (Ralph-style loop) # ──────────────────────────────────────────────────────────────── + def _goal_max_turns_from_config(self) -> int: + """Resolve the configured /goal turn budget for gateway sessions. + + GatewayRunner.config is a GatewayConfig dataclass, not the full + user config mapping. Top-level config blocks such as ``goals`` are + therefore only available through hermes_cli.config.load_config(). + """ + try: + goals_cfg = ( + (self.config or {}).get("goals", {}) + if isinstance(self.config, dict) + else getattr(self.config, "goals", {}) or {} + ) + if not goals_cfg: + from hermes_cli.config import load_config + + goals_cfg = (load_config() or {}).get("goals") or {} + return int(goals_cfg.get("max_turns", 20) or 20) + except Exception: + return 20 + def _get_goal_manager_for_event(self, event: "MessageEvent"): """Return a GoalManager bound to the session for this gateway event. @@ -8350,15 +8371,7 @@ class GatewayRunner: sid = getattr(session_entry, "session_id", None) or "" if not sid: return None, None - try: - goals_cfg = ( - (self.config or {}).get("goals", {}) - if isinstance(self.config, dict) - else getattr(self.config, "goals", {}) or {} - ) - max_turns = int(goals_cfg.get("max_turns", 20) or 20) - except Exception: - max_turns = 20 + max_turns = self._goal_max_turns_from_config() return GoalManager(session_id=sid, default_max_turns=max_turns), session_entry async def _handle_goal_command(self, event: "MessageEvent") -> str: @@ -8458,15 +8471,7 @@ class GatewayRunner: if not sid: return - try: - goals_cfg = ( - (self.config or {}).get("goals", {}) - if isinstance(self.config, dict) - else getattr(self.config, "goals", {}) or {} - ) - max_turns = int(goals_cfg.get("max_turns", 20) or 20) - except Exception: - max_turns = 20 + max_turns = self._goal_max_turns_from_config() mgr = GoalManager(session_id=sid, default_max_turns=max_turns) if not mgr.is_active(): diff --git a/tests/gateway/test_goal_max_turns_config.py b/tests/gateway/test_goal_max_turns_config.py new file mode 100644 index 0000000000..154485bd34 --- /dev/null +++ b/tests/gateway/test_goal_max_turns_config.py @@ -0,0 +1,62 @@ +import pytest + +from gateway.config import GatewayConfig, Platform, PlatformConfig +from gateway.platforms.base import MessageEvent, MessageType +from gateway.run import GatewayRunner +from gateway.session import SessionSource +from hermes_cli import goals + + +class _FakeSessionEntry: + session_id = "sid-gateway-goal-config" + + +class _FakeSessionStore: + def __init__(self): + self.entry = _FakeSessionEntry() + + def get_or_create_session(self, source): + return self.entry + + def _generate_session_key(self, source): + return "agent:main:discord:channel:goal-config" + + +@pytest.mark.asyncio +async def test_gateway_goal_uses_goals_max_turns_from_full_config(tmp_path, monkeypatch): + """Gateway /goal should honor top-level goals.max_turns from config.yaml.""" + home = tmp_path / ".hermes" + home.mkdir() + (home / "config.yaml").write_text("goals:\n max_turns: 7\n", encoding="utf-8") + monkeypatch.setenv("HERMES_HOME", str(home)) + goals._DB_CACHE.clear() + + runner = object.__new__(GatewayRunner) + runner.config = GatewayConfig( + platforms={Platform.DISCORD: PlatformConfig(enabled=True, token="token")} + ) + runner.session_store = _FakeSessionStore() + runner.adapters = {} + runner._queued_events = {} + + event = MessageEvent( + text="/goal ship the benchmark", + message_type=MessageType.TEXT, + source=SessionSource( + platform=Platform.DISCORD, + chat_id="chat-goal-config", + chat_type="channel", + user_id="user-goal-config", + ), + message_id="msg-goal-config", + ) + + response = await GatewayRunner._handle_goal_command(runner, event) + + try: + assert "⊙ Goal set (7-turn budget): ship the benchmark" in response + state = goals.GoalManager("sid-gateway-goal-config").state + assert state is not None + assert state.max_turns == 7 + finally: + goals._DB_CACHE.clear()