diff --git a/plugins/memory/honcho/client.py b/plugins/memory/honcho/client.py index c044d776d8d7..4112b0d8b2f4 100644 --- a/plugins/memory/honcho/client.py +++ b/plugins/memory/honcho/client.py @@ -855,6 +855,28 @@ class HonchoClientConfig: _honcho_client_slot: SingletonSlot = SingletonSlot() +_cached_timeout: float | None = None + + +def _resolve_timeout_from_sources(config: HonchoClientConfig | None) -> float: + """Resolve the effective timeout from env, config.yaml, and the explicit config.""" + timeout = config.timeout if config is not None else None + if timeout is None: + timeout = _resolve_optional_float(os.environ.get("HONCHO_TIMEOUT")) + if timeout is None: + try: + from hermes_cli.config import load_config + + hermes_cfg = load_config() + honcho_cfg = hermes_cfg.get("honcho", {}) + if isinstance(honcho_cfg, dict): + timeout = _resolve_optional_float( + honcho_cfg.get("timeout"), + honcho_cfg.get("request_timeout"), + ) + except Exception: + pass + return timeout if timeout is not None else _DEFAULT_HTTP_TIMEOUT def _apply_fresh_oauth_token(config: HonchoClientConfig) -> None: @@ -900,10 +922,20 @@ def get_honcho_client(config: HonchoClientConfig | None = None) -> Honcho: first calls (double-checked locking via ``SingletonSlot``), so racing threads can't each construct a client and leak the loser's connection. """ + global _cached_timeout cached = _honcho_client_slot.peek() if cached is not None: - _refresh_cached_oauth(cached, config) - return cached + # Detect timeout config changes in long-lived processes (gateway, + # dashboard). If the user changed the timeout after the client was + # built, rebuild with the new value. + new_timeout = _resolve_timeout_from_sources(config) + if new_timeout != _cached_timeout: + _honcho_client_slot.reset() + _cached_timeout = None + cached = None + else: + _refresh_cached_oauth(cached, config) + return cached if config is None: config = HonchoClientConfig.from_global_config() @@ -1014,6 +1046,8 @@ def get_honcho_client(config: HonchoClientConfig | None = None) -> Honcho: if resolved_timeout is not None: kwargs["timeout"] = resolved_timeout + global _cached_timeout + _cached_timeout = resolved_timeout return Honcho(**kwargs) return _honcho_client_slot.get(_build) @@ -1021,4 +1055,6 @@ def get_honcho_client(config: HonchoClientConfig | None = None) -> Honcho: def reset_honcho_client() -> None: """Reset the Honcho client singleton (useful for testing).""" + global _cached_timeout _honcho_client_slot.reset() + _cached_timeout = None diff --git a/tests/honcho_plugin/test_client.py b/tests/honcho_plugin/test_client.py index 85e87579260c..fc60b9370ece 100644 --- a/tests/honcho_plugin/test_client.py +++ b/tests/honcho_plugin/test_client.py @@ -735,6 +735,44 @@ class TestGetHonchoClient: mock_honcho.assert_called_once() assert mock_honcho.call_args.kwargs["timeout"] == 77.5 + @pytest.mark.skipif( + not importlib.util.find_spec("honcho"), + reason="honcho SDK not installed" + ) + def test_timeout_change_triggers_client_rebuild(self): + """Changing timeout config must rebuild the cached client.""" + fake_honcho_1 = MagicMock(name="Honcho_v1") + fake_honcho_2 = MagicMock(name="Honcho_v2") + cfg = HonchoClientConfig( + api_key="test-key", + workspace_id="hermes", + environment="production", + ) + + with patch("honcho.Honcho", return_value=fake_honcho_1) as mock_h1, \ + patch("hermes_cli.config.load_config", return_value={"honcho": {"timeout": 30}}): + client1 = get_honcho_client(cfg) + + assert client1 is fake_honcho_1 + assert mock_h1.call_args.kwargs["timeout"] == 30.0 + + # Same config — should return cached client (no rebuild) + with patch("honcho.Honcho", return_value=fake_honcho_2) as mock_h2, \ + patch("hermes_cli.config.load_config", return_value={"honcho": {"timeout": 30}}): + client2 = get_honcho_client(cfg) + + assert client2 is fake_honcho_1 # still cached + mock_h2.assert_not_called() + + # Changed timeout — must rebuild + with patch("honcho.Honcho", return_value=fake_honcho_2) as mock_h3, \ + patch("hermes_cli.config.load_config", return_value={"honcho": {"timeout": 300}}): + client3 = get_honcho_client(cfg) + + assert client3 is fake_honcho_2 # rebuilt + mock_h3.assert_called_once() + assert mock_h3.call_args.kwargs["timeout"] == 300.0 + class TestResolveSessionNameGatewayKey: """Regression tests for gateway_session_key priority in resolve_session_name.