From e8957babf4318ff69efbad5efcded6de9fb448d5 Mon Sep 17 00:00:00 2001 From: Erosika Date: Thu, 16 Jul 2026 12:59:50 -0400 Subject: [PATCH] feat(honcho): make latency-adding paths configurable queryRewrite (default off) gates the latest-message rewrite so the extra auxiliary LLM call is opt-in. firstTurnBaseWait and firstTurnDialecticWait expose the turn-1 bounded waits in seconds (0 disables). All three resolve host-block-first like every other field. Also pins per-host timeout resolution with tests. --- plugins/memory/honcho/__init__.py | 8 +++- plugins/memory/honcho/client.py | 34 ++++++++++++++++ tests/honcho_plugin/test_query_rewrite.py | 23 +++++++++++ tests/test_honcho_client_config.py | 47 +++++++++++++++++++++++ tests/test_honcho_startup_fail_open.py | 3 ++ 5 files changed, 113 insertions(+), 2 deletions(-) diff --git a/plugins/memory/honcho/__init__.py b/plugins/memory/honcho/__init__.py index f117014d0972..dd3ffe0bb8ab 100644 --- a/plugins/memory/honcho/__init__.py +++ b/plugins/memory/honcho/__init__.py @@ -256,6 +256,7 @@ class HonchoMemoryProvider(MemoryProvider): # B5: Cost-awareness turn counting and cadence self._turn_count = 0 + self._query_rewrite_enabled = False self._injection_frequency = "every-turn" # or "first-turn" self._context_cadence = 1 # minimum turns between context API calls self._dialectic_cadence = 1 # backwards-compat fallback; wizard writes 2 on new configs @@ -361,6 +362,9 @@ class HonchoMemoryProvider(MemoryProvider): self._injection_frequency = cfg.injection_frequency self._context_cadence = cfg.context_cadence self._dialectic_cadence = cfg.dialectic_cadence + self._query_rewrite_enabled = cfg.query_rewrite + self._FIRST_TURN_BASE_TIMEOUT = cfg.first_turn_base_wait + self._FIRST_TURN_DIALECTIC_CAP = cfg.first_turn_dialectic_wait self._dialectic_depth = max(1, min(cfg.dialectic_depth, 3)) self._dialectic_depth_levels = cfg.dialectic_depth_levels self._reasoning_heuristic = cfg.reasoning_heuristic @@ -510,7 +514,7 @@ class HonchoMemoryProvider(MemoryProvider): except Exception as e: logger.debug("Honcho context prewarm failed: %s", e) - if self._query_rewriter is None: + if self._query_rewriter is None or not self._query_rewrite_enabled: _prewarm_query = ( "Summarize what you know about this user. " "Focus on preferences, current projects, and working style." @@ -1225,7 +1229,7 @@ class HonchoMemoryProvider(MemoryProvider): is_cold = not self._base_context_cache results: list[str] = [] rewritten_query = "" - if use_query_rewrite and self._query_rewriter: + if use_query_rewrite and self._query_rewrite_enabled and self._query_rewriter: try: rewritten_query = self._query_rewriter(query).strip() except Exception as exc: diff --git a/plugins/memory/honcho/client.py b/plugins/memory/honcho/client.py index 9f69f278ef4b..208757247fab 100644 --- a/plugins/memory/honcho/client.py +++ b/plugins/memory/honcho/client.py @@ -145,6 +145,17 @@ def _parse_int_config(host_val, root_val, default: int) -> int: return default +def _parse_float_config(host_val, root_val, default: float) -> float: + """Parse a float config: host wins, then root, then default. Clamped ≥ 0.""" + for val in (host_val, root_val): + if val is not None: + try: + return max(0.0, float(val)) + except (ValueError, TypeError): + pass + return default + + def _parse_string_map(host_obj: dict, root_obj: dict, key: str) -> dict[str, str]: """Parse a string-to-string map with host-level whole-map override.""" source = host_obj[key] if key in host_obj else root_obj.get(key) @@ -368,6 +379,14 @@ class HonchoClientConfig: context_cadence: int = 1 # Minimum turns between dialectic prefetch fires (supplement layer cadence) dialectic_cadence: int = 1 + # Rewrite the latest user message into a retrieval query before dialectic. + # Off by default: adds one auxiliary LLM call per dialectic fire + # (model/timeout under auxiliary.memory_query_rewrite in config.yaml). + query_rewrite: bool = False + # Bounded synchronous waits on turn 1, in seconds. 0 disables the wait + # entirely (fully async first turn; context surfaces on later turns). + first_turn_base_wait: float = 3.0 + first_turn_dialectic_wait: float = 2.0 # Observation mode: legacy string shorthand ("directional" or "unified"). # Kept for backward compat; granular per-peer booleans below are preferred. observation_mode: str = "directional" @@ -627,6 +646,21 @@ class HonchoClientConfig: raw.get("dialecticCadence"), default=1, ), + query_rewrite=_resolve_bool( + host_block.get("queryRewrite"), + raw.get("queryRewrite"), + default=False, + ), + first_turn_base_wait=_parse_float_config( + host_block.get("firstTurnBaseWait"), + raw.get("firstTurnBaseWait"), + default=3.0, + ), + first_turn_dialectic_wait=_parse_float_config( + host_block.get("firstTurnDialecticWait"), + raw.get("firstTurnDialecticWait"), + default=2.0, + ), # Migration guard: existing configs without an explicit # observationMode keep the old "unified" default so users # aren't silently switched to full bidirectional observation. diff --git a/tests/honcho_plugin/test_query_rewrite.py b/tests/honcho_plugin/test_query_rewrite.py index ad4c4823a0ed..8f56959dda73 100644 --- a/tests/honcho_plugin/test_query_rewrite.py +++ b/tests/honcho_plugin/test_query_rewrite.py @@ -100,6 +100,7 @@ def test_long_input_keeps_both_ends_with_a_hard_bound(): def _provider(query_rewriter, *, depth=1): provider = HonchoMemoryProvider(query_rewriter=query_rewriter) + provider._query_rewrite_enabled = True provider._manager = MagicMock() provider._manager.dialectic_query.return_value = "memory synthesis" provider._session_key = "test-session" @@ -189,6 +190,7 @@ def test_first_user_message_is_not_shadowed_by_generic_dialectic_prewarm(): enabled=True, recall_mode="hybrid", timeout=1, + query_rewrite=True, ) with ( @@ -237,3 +239,24 @@ def test_query_rewrite_has_an_independent_auxiliary_model_config(): assert task_config["provider"] == "auto" assert task_config["timeout"] == 8 assert TASK_KEY in {key for key, _name, _description in _AUX_TASKS} + + +def test_query_rewrite_disabled_by_default(): + """queryRewrite defaults OFF — the rewriter must not add an LLM call.""" + rewriter = MagicMock(return_value="What does the user prefer?") + provider = _provider(rewriter) + provider._query_rewrite_enabled = False + + provider._run_dialectic_depth("what did we decide?") + + rewriter.assert_not_called() + provider._manager.dialectic_query.assert_called_once() + + +def test_config_defaults_keep_latency_additions_off(): + from plugins.memory.honcho.client import HonchoClientConfig + + cfg = HonchoClientConfig(api_key="k", enabled=True) + assert cfg.query_rewrite is False + assert cfg.first_turn_base_wait == 3.0 + assert cfg.first_turn_dialectic_wait == 2.0 diff --git a/tests/test_honcho_client_config.py b/tests/test_honcho_client_config.py index f7b1efa151cc..78516cdb8da8 100644 --- a/tests/test_honcho_client_config.py +++ b/tests/test_honcho_client_config.py @@ -125,3 +125,50 @@ def test_save_config_sets_owner_only_permissions(tmp_path, monkeypatch): assert config_file.exists() mode = stat.S_IMODE(config_file.stat().st_mode) assert mode == 0o600, f"Expected 0o600 (owner-only), got {oct(mode)}" + + +class TestLatencyFlagResolution: + def test_defaults(self, tmp_path, monkeypatch): + monkeypatch.delenv('HONCHO_BASE_URL', raising=False) + config_path = tmp_path / 'config.json' + config_path.write_text(json.dumps({'apiKey': 'k'})) + cfg = HonchoClientConfig.from_global_config(config_path=config_path) + assert cfg.query_rewrite is False + assert cfg.first_turn_base_wait == 3.0 + assert cfg.first_turn_dialectic_wait == 2.0 + + def test_host_block_wins(self, tmp_path, monkeypatch): + monkeypatch.delenv('HONCHO_BASE_URL', raising=False) + config_path = tmp_path / 'config.json' + config_path.write_text(json.dumps({ + 'apiKey': 'k', + 'queryRewrite': False, + 'firstTurnBaseWait': 3, + 'hosts': {'hermes': { + 'queryRewrite': True, + 'firstTurnBaseWait': 0, + 'firstTurnDialecticWait': 0.5, + }}, + })) + cfg = HonchoClientConfig.from_global_config(config_path=config_path) + assert cfg.query_rewrite is True + assert cfg.first_turn_base_wait == 0.0 + assert cfg.first_turn_dialectic_wait == 0.5 + + def test_per_host_timeout_wins_over_global(self, tmp_path, monkeypatch): + monkeypatch.delenv('HONCHO_TIMEOUT', raising=False) + config_path = tmp_path / 'config.json' + config_path.write_text(json.dumps({ + 'apiKey': 'k', + 'timeout': 30, + 'hosts': {'hermes': {'timeout': 5}}, + })) + cfg = HonchoClientConfig.from_global_config(config_path=config_path) + assert cfg.timeout == 5.0 + + def test_timeout_falls_back_to_global(self, tmp_path, monkeypatch): + monkeypatch.delenv('HONCHO_TIMEOUT', raising=False) + config_path = tmp_path / 'config.json' + config_path.write_text(json.dumps({'apiKey': 'k', 'timeout': 30})) + cfg = HonchoClientConfig.from_global_config(config_path=config_path) + assert cfg.timeout == 30.0 diff --git a/tests/test_honcho_startup_fail_open.py b/tests/test_honcho_startup_fail_open.py index 0b0404bfefc6..0b80a635c2f9 100644 --- a/tests/test_honcho_startup_fail_open.py +++ b/tests/test_honcho_startup_fail_open.py @@ -25,6 +25,9 @@ def _configured_hybrid_config() -> _FakeHonchoConfig: injection_frequency="every-turn", context_cadence=1, dialectic_cadence=1, + query_rewrite=False, + first_turn_base_wait=3.0, + first_turn_dialectic_wait=2.0, dialectic_depth=1, dialectic_depth_levels=None, reasoning_heuristic=True,