fix(plugins): rebuild Honcho client when timeout config changes

The Honcho client singleton cached the HTTP timeout at first build.
In long-lived processes (gateway, dashboard), changing the timeout
via config.yaml or HONCHO_TIMEOUT had no effect until restart.

Track the resolved timeout alongside the cached client and compare
on each get_honcho_client() call. When the timeout differs, reset
the singleton so the next call rebuilds with the new value.

Fixes #57347
This commit is contained in:
liuhao1024 2026-07-03 09:09:10 +08:00 committed by Teknium
parent 73a4574ede
commit e5bebe2cad
2 changed files with 76 additions and 2 deletions

View file

@ -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

View file

@ -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.