From c575351d9ac50f8e976a6c910e07eedd49e09e32 Mon Sep 17 00:00:00 2001 From: kshitij <82637225+kshitijk4poor@users.noreply.github.com> Date: Sun, 2 Aug 2026 22:41:02 +0530 Subject: [PATCH] fix(mem): 5s force floor so delegate-batch closes don't stack trims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Efficiency-pass follow-up on the #66355 salvage: force=True bypassed the cooldown entirely, and AIAgent.close() fires a forced trim for EVERY in-process child subagent close (delegate_tool child.close(), parent close step 5). A delegate batch of N children closing back-to-back in the gateway process stacked N+1 uncooled full gc.collect()+malloc_trim passes (50-500ms each with a large live heap). Forced trims now honor a 5s floor — bursts coalesce, the parent's final close-trim still fires. Guard test mutation-checked (floor zeroed -> test fails). --- hermes_cli/mem_trim.py | 12 ++++++++++ tests/hermes_cli/test_mem_trim.py | 38 ++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/hermes_cli/mem_trim.py b/hermes_cli/mem_trim.py index 34d2ce364e..ad54f59a38 100644 --- a/hermes_cli/mem_trim.py +++ b/hermes_cli/mem_trim.py @@ -200,6 +200,18 @@ def trim_memory( ) if not force and _last_trim_monotonic and now - _last_trim_monotonic < cooldown: return False + # Even forced trims honor a short floor: AIAgent.close() forces a trim, + # and delegate batches close N child subagents back-to-back in the SAME + # process — without a floor that stacks N+1 uncooled full gc.collect() + # passes (50-500ms each in a large gateway process). 5s coalesces the + # burst while keeping the parent's final close-trim effective. + _FORCE_FLOOR_SECONDS = 5.0 + if ( + force + and _last_trim_monotonic + and now - _last_trim_monotonic < _FORCE_FLOOR_SECONDS + ): + return False # Record the attempt before calling into libc so repeated failures do not # turn every turn boundary into an expensive full collection. _last_trim_monotonic = now diff --git a/tests/hermes_cli/test_mem_trim.py b/tests/hermes_cli/test_mem_trim.py index dd5b38eb3c..3c94f23f07 100644 --- a/tests/hermes_cli/test_mem_trim.py +++ b/tests/hermes_cli/test_mem_trim.py @@ -108,7 +108,10 @@ def test_force_logs_even_when_periodic_log_sampling_skips(monkeypatch, caplog): monkeypatch.setattr(mem_trim.gc, "collect", lambda: None) monkeypatch.setattr(mem_trim, "_malloc_trim", lambda _pad: 1) monkeypatch.setattr(mem_trim, "_config_settings", lambda: (True, 0.0, 99, 1.0)) - monkeypatch.setattr(mem_trim.time, "monotonic", lambda: 100.0) + # Two ticks: the forced call comes after the 5s force floor so it runs + # (the floor exists to coalesce burst closes, not to mute logging). + _ticks = iter([100.0, 110.0]) + monkeypatch.setattr(mem_trim.time, "monotonic", lambda: next(_ticks, 110.0)) monkeypatch.setattr( mem_trim, "collect_memory_snapshot", @@ -178,3 +181,36 @@ def test_libc_failure_is_fail_open_and_rate_limited(monkeypatch): assert mem_trim._last_trim_monotonic == 100.0 assert mem_trim.trim_memory(cooldown_seconds=60) is False assert trim.call_count == 1 + + +def test_force_floor_coalesces_burst_closes(monkeypatch): + """A delegate batch closes N child agents back-to-back, each forcing a + trim — the short force floor must coalesce the burst instead of stacking + N uncooled full gc.collect() passes in the same process.""" + collect = Mock() + trim = Mock(return_value=1) + monkeypatch.setattr(mem_trim.gc, "collect", collect) + monkeypatch.setattr(mem_trim, "_malloc_trim", trim) + monkeypatch.setattr(mem_trim, "_config_settings", lambda: (True, 0.0, 1, 0.0)) + monkeypatch.setattr( + mem_trim, + "collect_memory_snapshot", + lambda: {"rss_kib": 4096, "rss_anon_kib": 3072, "thread_count": 3}, + ) + monkeypatch.setattr(mem_trim, "_last_trim_monotonic", 0.0) + + # t=100: first forced close runs. + monkeypatch.setattr(mem_trim.time, "monotonic", lambda: 100.0) + assert mem_trim.trim_memory(force=True, reason="agent close") is True + assert trim.call_count == 1 + + # t=101..103: three more child closes inside the floor — all coalesced. + for t in (101.0, 102.0, 103.0): + monkeypatch.setattr(mem_trim.time, "monotonic", lambda t=t: t) + assert mem_trim.trim_memory(force=True, reason="agent close") is False + assert trim.call_count == 1, "burst closes must not stack forced trims" + + # t=106: past the floor — the parent's final close-trim still fires. + monkeypatch.setattr(mem_trim.time, "monotonic", lambda: 106.0) + assert mem_trim.trim_memory(force=True, reason="agent close") is True + assert trim.call_count == 2