From b5f072e2ceab1d050a5b8b9c4a29e482bbc1fe26 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:46:10 -0700 Subject: [PATCH] fix(agent): manual /compress "nothing to compress" gate stays gateway-only The shared core applied `has_content_to_compress(head) is False -> nothing_to_do` on every surface, where origin/main only had it in the gateway handler. That predicate only knows the local summarizer's window: on CLI/TUI/ACP, `_compress_context(force=True)` still routes codex_app_server sessions to native compaction before any local-compressor check, and `ContextCompressor.compress` commits the phase-1 tool-result prune / blank-echo drop even when no summary window exists -- so the gate wrongly skipped real work there. It is now an opt-in `skip_without_window` that only the gateway passes, restoring each surface's prior behavior. Review follow-up on #109610. --- agent/conversation_compression_manual.py | 10 +++++++--- gateway/slash_commands_session.py | 2 +- .../test_conversation_compression_manual.py | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/agent/conversation_compression_manual.py b/agent/conversation_compression_manual.py index b71086774f..d8fdbfa6b6 100644 --- a/agent/conversation_compression_manual.py +++ b/agent/conversation_compression_manual.py @@ -68,7 +68,7 @@ def estimate_request_tokens(agent: Any, messages: Sequence[Dict[str, Any]]) -> i def compress_now( agent: Any, history: Sequence[Dict[str, Any]], request: CompressRequest, *, - system_message: Any = None, task_id: str = "default", + system_message: Any = None, task_id: str = "default", skip_without_window: bool = False, ) -> CompressResult: """Run one manual compression of ``history`` on ``agent`` and return the outcome; the caller installs ``after_messages`` (and re-anchors session ids) — history is never mutated here. @@ -78,7 +78,11 @@ def compress_now( discarded; otherwise the caller must call ``finalize_context_engine_compression_notification(agent, committed=True)`` once its own history transaction commits (``committed=False`` on failure). ``system_message=None`` makes ``_compress_context`` rebuild the prompt; passing the cached prompt - duplicated the identity block (#15281).""" + duplicated the identity block (#15281). ``skip_without_window`` (gateway) answers ``nothing_to_do`` + when the local compressor sees no summarizable middle; the in-process surfaces leave it off because + ``_compress_context`` still does useful work there — codex_app_server native compaction, and the + phase-1 tool-result prune / blank-echo drop that ``ContextCompressor.compress`` commits even when no + summary window exists.""" from agent.conversation_compression import finalize_context_engine_compression_notification from agent.manual_compression_feedback import summarize_manual_compression from hermes_cli.partial_compress import ( @@ -97,7 +101,7 @@ def compress_now( compressor = getattr(agent, "context_compressor", None) has_content = getattr(compressor, "has_content_to_compress", None) - if callable(has_content) and has_content(head) is False: + if skip_without_window and callable(has_content) and has_content(head) is False: return CompressResult("nothing_to_do", before, before, before_tokens, before_tokens, request) try: compressed, _ = agent._compress_context( diff --git a/gateway/slash_commands_session.py b/gateway/slash_commands_session.py index 3f967deaf6..e951e97a3b 100644 --- a/gateway/slash_commands_session.py +++ b/gateway/slash_commands_session.py @@ -550,7 +550,7 @@ class GatewaySessionCommandsMixin: # Not a bare run_in_executor: the profile secret scope is a contextvar the default # executor hop would drop, failing aux-client credential resolution closed. result = await self._run_in_executor_with_context( - lambda: compress_now(tmp_agent, msgs, request, system_message="")) + lambda: compress_now(tmp_agent, msgs, request, system_message="", skip_without_window=True)) if result.status == "nothing_to_do": return t("gateway.compress.nothing_to_do") if result.status != "compressed": diff --git a/tests/agent/test_conversation_compression_manual.py b/tests/agent/test_conversation_compression_manual.py index 89741f62a5..267c0bce56 100644 --- a/tests/agent/test_conversation_compression_manual.py +++ b/tests/agent/test_conversation_compression_manual.py @@ -87,6 +87,25 @@ def test_every_surface_honours_preview_without_compressing(surface, monkeypatch) agent._compress_context.assert_not_called() +def test_windowless_gate_is_gateway_only_so_in_process_surfaces_still_reach_compress_context(): + """``has_content_to_compress`` only knows the local summary window; codex_app_server native compaction + and the phase-1 tool-result prune inside ``_compress_context`` do useful work without one, so CLI/TUI/ + ACP (the default) must not short-circuit on it. The gateway keeps its historical early answer.""" + agent, history = _agent(), _history() + agent.context_compressor = MagicMock() + agent.context_compressor.has_content_to_compress.return_value = False + agent._compress_context.return_value = (history[:-1], "") # e.g. a pruned tool result, no summary + + default = compress_now(agent, history, parse_compress_args("")) + assert default.status == "compressed" and default.removed == 1 + agent._compress_context.assert_called_once() + + agent._compress_context.reset_mock() + gateway = compress_now(agent, history, parse_compress_args(""), system_message="", skip_without_window=True) + assert gateway.status == "nothing_to_do" and gateway.after_messages == history + agent._compress_context.assert_not_called() + + def _coro(value): async def _inner(*_a, **_k): return value