diff --git a/gateway/run.py b/gateway/run.py index 1a92da6beb..e184be76fb 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -25436,7 +25436,11 @@ def _start_gateway_housekeeping(stop_event: threading.Event, adapters=None, loop trim_memory(reason="messaging gateway housekeeping") except Exception as exc: - logger.warning( + # debug, not warning: sibling housekeeping branches all log + # failures at debug, and a persistent failure (e.g. broken + # import after a partial update) would otherwise warn every + # 60s forever. + logger.debug( "gateway housekeeping memory trim failed: %s: %s", type(exc).__name__, exc, diff --git a/hermes_cli/mem_trim.py b/hermes_cli/mem_trim.py index ff15bea2ec..34d2ce364e 100644 --- a/hermes_cli/mem_trim.py +++ b/hermes_cli/mem_trim.py @@ -37,9 +37,14 @@ def _config_settings() -> tuple[bool, float, int, float]: log_every_n: Any = _DEFAULT_LOG_EVERY_N info_log_min_delta_mb: Any = _DEFAULT_INFO_LOG_MIN_DELTA_MB try: - from hermes_cli.config import load_config + # Read-only access: settings are only .get()ed and coerced, never + # mutated — use the no-deepcopy variant. This runs on EVERY trim + # attempt (before the cooldown check), and generating a full-config + # deepcopy per attempt is exactly the allocator garbage this module + # exists to release. + from hermes_cli.config import load_config_readonly - config = load_config() or {} + config = load_config_readonly() or {} context = config.get("context") if isinstance(config, dict) else None settings = context.get("memory_trim") if isinstance(context, dict) else None if isinstance(settings, dict): diff --git a/tests/hermes_cli/test_mem_trim.py b/tests/hermes_cli/test_mem_trim.py index bf6936022f..dd5b38eb3c 100644 --- a/tests/hermes_cli/test_mem_trim.py +++ b/tests/hermes_cli/test_mem_trim.py @@ -144,7 +144,7 @@ def test_config_cooldown_controls_rate_limit(monkeypatch): monkeypatch.setattr(mem_trim, "_last_trim_monotonic", 1.0) monkeypatch.setattr(mem_trim.time, "monotonic", lambda: 100.0) monkeypatch.setattr( - "hermes_cli.config.load_config", + "hermes_cli.config.load_config_readonly", lambda: { "context": { "memory_trim": {"enabled": True, "cooldown_seconds": 120.0} @@ -161,7 +161,7 @@ def test_legacy_environment_switch_does_not_control_behavior(monkeypatch): monkeypatch.setattr(mem_trim, "_malloc_trim", trim) monkeypatch.setenv("HERMES_DISABLE_MEMORY_TRIM", "1") monkeypatch.setattr( - "hermes_cli.config.load_config", + "hermes_cli.config.load_config_readonly", lambda: {"context": {"memory_trim": {"enabled": True}}}, ) diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index b2d8e918fa..c5879a0427 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -14186,7 +14186,7 @@ def test_reap_idle_sessions_logs_trim_failure(monkeypatch, caplog): monkeypatch.setattr(mem_trim, "trim_memory", lambda **_kw: (_ for _ in ()).throw(RuntimeError("boom"))) server._sessions.clear() try: - with caplog.at_level("WARNING", logger="tui_gateway.server"): + with caplog.at_level("DEBUG", logger="tui_gateway.server"): server._reap_idle_sessions() assert "idle reaper memory trim failed: RuntimeError: boom" in caplog.text finally: @@ -15610,6 +15610,12 @@ def test_prompt_submit_releases_old_history_before_heap_trim(monkeypatch): frame = inspect.currentframe() assert frame is not None and frame.f_back is not None caller_locals = frame.f_back.f_locals + # Loud, not vacuous: if the production locals are ever renamed, fail + # the test instead of silently reading None and "passing". + assert "history" in caller_locals and "run_kwargs" in caller_locals, ( + "expected locals not found in _run_prompt_submit's finally frame — " + "renamed? update this test" + ) observed["history"] = caller_locals.get("history") observed["run_kwargs"] = caller_locals.get("run_kwargs") diff --git a/tui_gateway/server.py b/tui_gateway/server.py index f2e8fb6472..015ec90df3 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -1064,7 +1064,9 @@ def _reap_idle_sessions() -> None: trim_memory(reason="idle reaper periodic trim") except Exception as exc: - logger.warning( + # debug, not warning — persistent failure would repeat every reaper + # scan (300s) forever; sibling failure branches log at debug. + logger.debug( "idle reaper memory trim failed: %s: %s", type(exc).__name__, exc ) diff --git a/tui_gateway/slash_worker.py b/tui_gateway/slash_worker.py index 85bbb5e598..7066a62f43 100644 --- a/tui_gateway/slash_worker.py +++ b/tui_gateway/slash_worker.py @@ -183,7 +183,9 @@ def main(): trim_memory(reason="slash worker command completion") except Exception as exc: - logger.warning( + # debug, not warning — a persistent failure would repeat on + # every slash command forever. + logger.debug( "slash worker memory trim failed: %s: %s", type(exc).__name__, exc,