From d111faa3a76daf0357bc9d9958aef9976089652b Mon Sep 17 00:00:00 2001 From: devatnull Date: Mon, 29 Jun 2026 08:03:05 +0300 Subject: [PATCH] fix: preserve busy steer env override --- gateway/run.py | 29 ++++++++++++++++--------- tests/gateway/test_busy_session_ack.py | 30 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/gateway/run.py b/gateway/run.py index b185ea558b1..dbabee41bde 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -1563,8 +1563,16 @@ if _config_path.exists(): os.environ["HERMES_GATEWAY_BUSY_TEXT_MODE"] = str(_display_cfg["busy_text_mode"]) if "busy_ack_enabled" in _display_cfg: os.environ["HERMES_GATEWAY_BUSY_ACK_ENABLED"] = str(_display_cfg["busy_ack_enabled"]) - if "busy_steer_ack_enabled" in _display_cfg: - os.environ["HERMES_GATEWAY_BUSY_STEER_ACK_ENABLED"] = str(_display_cfg["busy_steer_ack_enabled"]) + # This process-level env var is documented as an override for + # service managers, so preserve it when already set. Other display + # bridges stay config-authoritative for backwards compatibility. + if ( + "busy_steer_ack_enabled" in _display_cfg + and "HERMES_GATEWAY_BUSY_STEER_ACK_ENABLED" not in os.environ + ): + os.environ["HERMES_GATEWAY_BUSY_STEER_ACK_ENABLED"] = str( + _display_cfg["busy_steer_ack_enabled"] + ) # Timezone: bridge config.yaml → HERMES_TIMEZONE env var. _tz_cfg = _cfg.get("timezone", "") if _tz_cfg and isinstance(_tz_cfg, str): @@ -5332,6 +5340,15 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew logger.debug("Busy ack suppressed for session %s", session_key) return True # input still processed, just no ack sent + # Debounce before consulting config-heavy display settings. Rapid + # follow-ups should be processed but should not trigger another config + # read just to discover that no ack will be sent. + _BUSY_ACK_COOLDOWN = 30 + now = time.time() + last_ack = self._busy_ack_ts.get(session_key, 0) + if now - last_ack < _BUSY_ACK_COOLDOWN: + return True # interrupt sent (if not queue), ack already delivered recently + from gateway.display_config import resolve_display_setting platform_key = _platform_config_key(event.source.platform) @@ -5356,14 +5373,6 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew logger.debug("Busy steer ack suppressed for session %s", session_key) return True - # Debounce: only send an acknowledgment once every 30 seconds per session - # to avoid spamming the user when they send multiple messages quickly - _BUSY_ACK_COOLDOWN = 30 - now = time.time() - last_ack = self._busy_ack_ts.get(session_key, 0) - if now - last_ack < _BUSY_ACK_COOLDOWN: - return True # interrupt sent (if not queue), ack already delivered recently - self._busy_ack_ts[session_key] = now # Build a status-rich acknowledgment. Mobile chat defaults keep this diff --git a/tests/gateway/test_busy_session_ack.py b/tests/gateway/test_busy_session_ack.py index 96a4684a13f..ed3abe6d7a8 100644 --- a/tests/gateway/test_busy_session_ack.py +++ b/tests/gateway/test_busy_session_ack.py @@ -358,6 +358,36 @@ class TestBusySessionAck: adapter._send_with_retry.assert_not_called() assert sk not in adapter._pending_messages + @pytest.mark.asyncio + async def test_busy_ack_debounce_skips_steer_ack_config_load(self, monkeypatch): + """Rapid follow-ups should not reload display config when ack is debounced.""" + import gateway.run as _gr + + def _boom(): + raise AssertionError("config should not be loaded inside ack cooldown") + + monkeypatch.delenv("HERMES_GATEWAY_BUSY_STEER_ACK_ENABLED", raising=False) + monkeypatch.setattr(_gr, "_load_gateway_config", _boom) + + runner, sentinel = _make_runner() + runner._busy_input_mode = "steer" + adapter = _make_adapter() + + event = _make_event(text="rapid steer") + sk = build_session_key(event.source) + runner.adapters[event.source.platform] = adapter + + agent = MagicMock() + agent.steer = MagicMock(return_value=True) + runner._running_agents[sk] = agent + runner._busy_ack_ts[sk] = time.time() + + result = await runner._handle_active_session_busy_message(event, sk) + + assert result is True + agent.steer.assert_called_once_with("rapid steer") + adapter._send_with_retry.assert_not_called() + @pytest.mark.asyncio async def test_steer_mode_falls_back_to_queue_when_agent_rejects(self): """If agent.steer() returns False, fall back to queue behavior."""