From f4896015c113bac60de7d233585a139a3d78bf5b Mon Sep 17 00:00:00 2001 From: McHermes Date: Tue, 21 Jul 2026 14:34:07 +0000 Subject: [PATCH] fix(compression): ignore assistant handoff summaries in tail anchor Assistant-role compaction summaries were treated as the last visible assistant reply after head protection decayed. That pulled the tail boundary back to the summary itself and left zero new turns to summarize. Exclude internal context summaries from both the visible-reply search and the assistant fallback, mirroring the existing user-role summary exclusion. --- agent/context_compressor.py | 12 ++++++-- .../test_compressor_assistant_tail_anchor.py | 29 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/agent/context_compressor.py b/agent/context_compressor.py index a16ec913461..2884e057ba7 100644 --- a/agent/context_compressor.py +++ b/agent/context_compressor.py @@ -2988,7 +2988,13 @@ This compaction should PRIORITISE preserving all information related to the focu indicators and aren't what the reporter means by "the output of the last message you sent" (#29824). - Falling back to the most recent assistant message of ANY kind + Context-compaction handoff banners can also carry + ``role="assistant"``. They are internal continuity state, not a + user-visible reply, so ignore them both as text-bearing anchors and + as candidates for the fallback below. This mirrors the user-role + summary exclusion in ``_find_last_user_message_idx``. + + Falling back to the most recent non-summary assistant message of ANY kind only kicks in when no content-bearing assistant message exists in the compressible region — typically a fresh session that just started a multi-step tool sequence with no prior reply @@ -2998,7 +3004,9 @@ This compaction should PRIORITISE preserving all information related to the focu last_any = -1 for i in range(len(messages) - 1, head_end - 1, -1): msg = messages[i] - if msg.get("role") != "assistant": + if msg.get("role") != "assistant" or self._is_context_summary_content( + msg.get("content") + ): continue if last_any < 0: last_any = i diff --git a/tests/agent/test_compressor_assistant_tail_anchor.py b/tests/agent/test_compressor_assistant_tail_anchor.py index a8be6dc3fef..0f870a83282 100644 --- a/tests/agent/test_compressor_assistant_tail_anchor.py +++ b/tests/agent/test_compressor_assistant_tail_anchor.py @@ -74,6 +74,35 @@ def compressor(): class TestFindLastAssistantMessageIdx: + def test_skips_assistant_role_context_summary_marker(self, compressor): + """A persisted assistant-role handoff is internal continuity state, + not the last reply the user saw. Tool-call-only assistant messages + after it must remain eligible for the fallback anchor.""" + from agent.context_compressor import SUMMARY_PREFIX + + messages = [ + {"role": "assistant", "content": f"{SUMMARY_PREFIX}\nold handoff"}, + {"role": "user", "content": "continue the task"}, + {"role": "assistant", "content": None, + "tool_calls": [{"function": {"name": "t", + "arguments": "{}"}}]}, + {"role": "tool", "content": "result", "tool_call_id": "c1"}, + ] + assert compressor._find_last_assistant_message_idx( + messages, head_end=0 + ) == 2 + + def test_all_assistant_messages_are_summaries_returns_minus_one(self, compressor): + from agent.context_compressor import SUMMARY_PREFIX + + messages = [ + {"role": "assistant", "content": f"{SUMMARY_PREFIX}\nold handoff"}, + {"role": "user", "content": "continue the task"}, + ] + assert compressor._find_last_assistant_message_idx( + messages, head_end=0 + ) == -1 + def test_finds_content_bearing_assistant(self, compressor): messages = [ {"role": "system", "content": "sys"},