From 4ba2608524fed4c94bb5b535fc26d7d483e333db Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Tue, 25 Aug 2026 14:36:05 +0530 Subject: [PATCH] fix(compressor): widen empty-content abort to sibling no-response shapes + snapshot state field Follow-up to PR #94531 salvage: - classify the auxiliary boundary's terminal 'None response' / 'invalid response' errors (#7264) into the same empty-content abort carve-out so those shapes also preserve the session (#94459's wider classification, sibling shapes from #94448) - register _last_summary_empty_content_failure in _COMPRESSOR_ATTEMPT_STATE_FIELDS so pre-commit hard-cancel rollback restores the flag (conversation_compression snapshot allow-list) - tests: cooldown re-entry keeps aborting; both sibling shapes abort - attribution: map zhangyswx@163.com -> YusenZhang0601 --- agent/context_compressor.py | 10 ++++- agent/conversation_compression.py | 1 + contributors/emails/zhangyswx@163.com | 2 + tests/agent/test_context_compressor.py | 55 ++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 contributors/emails/zhangyswx@163.com diff --git a/agent/context_compressor.py b/agent/context_compressor.py index b21f16e413..93dd7ce61d 100644 --- a/agent/context_compressor.py +++ b/agent/context_compressor.py @@ -5150,8 +5150,14 @@ This compaction should PRIORITISE preserving all information related to the focu _is_streaming_closed = _is_connection_error(e) # Provider returned HTTP 200 with empty or whitespace body (e.g. # degraded proxy channel / upstream provider fault; #94448). - _is_empty_content = ( - isinstance(e, RuntimeError) and "empty content" in _err_str + _is_empty_content = isinstance(e, RuntimeError) and ( + "empty content" in _err_str + # Sibling terminal "no usable response" shapes from the + # auxiliary boundary's _validate_llm_response (#7264): a None + # response or a malformed/missing choices[0].message — same + # degraded-provider class (#94448). + or "llm returned none response" in _err_str + or "llm returned invalid response" in _err_str ) # Authentication, permission, and exhausted-quota failures are NOT # transient or fixable by retrying the same request. Flag them so diff --git a/agent/conversation_compression.py b/agent/conversation_compression.py index 012600bca8..2e5c0cbde2 100644 --- a/agent/conversation_compression.py +++ b/agent/conversation_compression.py @@ -289,6 +289,7 @@ _COMPRESSOR_ATTEMPT_STATE_FIELDS = ( "_last_compress_aborted", "_last_summary_auth_failure", "_last_summary_network_failure", + "_last_summary_empty_content_failure", "_last_aux_model_failure_error", "_last_aux_model_failure_model", "_summary_model_fallen_back", diff --git a/contributors/emails/zhangyswx@163.com b/contributors/emails/zhangyswx@163.com new file mode 100644 index 0000000000..02824e62b7 --- /dev/null +++ b/contributors/emails/zhangyswx@163.com @@ -0,0 +1,2 @@ +YusenZhang0601 +# PR #94531 salvage (#94448) diff --git a/tests/agent/test_context_compressor.py b/tests/agent/test_context_compressor.py index 9736e2ce09..d977eb9445 100644 --- a/tests/agent/test_context_compressor.py +++ b/tests/agent/test_context_compressor.py @@ -966,6 +966,61 @@ class TestAuthFailureAborts: assert c._last_summary_fallback_used is False assert c._last_summary_dropped_count == 0 + # Cooldown re-entry must keep aborting, same as network/auth — + # _generate_summary() returns None from the cooldown early-return + # without re-asserting the flag, so compress() must still see it. + second = c.compress(msgs, current_tokens=999999) + assert second == msgs + assert c._last_compress_aborted is True + assert c._last_summary_fallback_used is False + + def test_auxiliary_none_response_aborts_compression(self): + """Sibling shape (#94459, from #7264): the auxiliary boundary's own + terminal "None response" error is the same degraded-provider class + and must ABORT, not fall through to the destructive fallback.""" + with patch("agent.context_compressor.get_model_context_length", return_value=100000): + c = ContextCompressor( + model="test", + quiet_mode=True, + protect_first_n=2, + protect_last_n=2, + abort_on_summary_failure=False, + ) + msgs = self._msgs(12) + with patch( + "agent.context_compressor.call_llm", + side_effect=RuntimeError("Auxiliary compression: LLM returned None response"), + ): + result = c.compress(msgs, current_tokens=999999, force=True) + assert result == msgs + assert c._last_compress_aborted is True + assert c._last_summary_empty_content_failure is True + + def test_auxiliary_invalid_response_aborts_compression(self): + """Sibling shape (#94459, from #7264): malformed/missing + choices[0].message terminal error must ABORT the same way.""" + with patch("agent.context_compressor.get_model_context_length", return_value=100000): + c = ContextCompressor( + model="test", + quiet_mode=True, + protect_first_n=2, + protect_last_n=2, + abort_on_summary_failure=False, + ) + msgs = self._msgs(12) + with patch( + "agent.context_compressor.call_llm", + side_effect=RuntimeError( + "Auxiliary compression: LLM returned invalid response " + "(type=str): 'oops'. Expected object with .choices[0].message " + "— check provider adapter or custom endpoint compatibility." + ), + ): + result = c.compress(msgs, current_tokens=999999, force=True) + assert result == msgs + assert c._last_compress_aborted is True + assert c._last_summary_empty_content_failure is True + class TestSummaryFallbackToMainModel: """When ``summary_model`` differs from the main model and the summary LLM