fix(compressor): skip context-summary markers as last-user tail anchor
A context-compaction handoff banner is inserted with role="user" when the protected head ends in an assistant/tool message. On a resumed or multi-compaction session, _find_last_user_message_idx would return that banner as the latest user turn, so _ensure_last_user_message_in_tail anchored the tail to the summary and rolled the genuine last user message into the next compaction — the exact active-task loss the anchor exists to prevent (#10896/#22523). Reuse the existing _is_context_summary_content helper to skip summary banners when locating the last real user message. Salvaged from #36626 by Frank Song (issue #36624). The PR's other two changes (demoting completed tool results inside the protected tail; a preflight compression_exhausted result) are superseded on current main by the min_tail floor (#39170), the no-op compression counting (#40803), and the existing 413/disabled terminal-error paths.
This commit is contained in:
parent
500c2b1e46
commit
ee710db135
2 changed files with 59 additions and 2 deletions
|
|
@ -2224,9 +2224,21 @@ This compaction should PRIORITISE preserving all information related to the focu
|
|||
def _find_last_user_message_idx(
|
||||
self, messages: List[Dict[str, Any]], head_end: int
|
||||
) -> int:
|
||||
"""Return the index of the last user-role message at or after *head_end*, or -1."""
|
||||
"""Return the index of the last user-role message at or after *head_end*, or -1.
|
||||
|
||||
A context-compaction handoff banner can be inserted as a ``role="user"``
|
||||
message (see the summary-role selection in ``compress``). It is internal
|
||||
continuity state, not a real user turn, so it must not be picked as the
|
||||
tail anchor — otherwise ``_ensure_last_user_message_in_tail`` protects
|
||||
the summary and rolls the genuine last user message into the next
|
||||
compaction, re-triggering the active-task loss the anchor exists to
|
||||
prevent.
|
||||
"""
|
||||
for i in range(len(messages) - 1, head_end - 1, -1):
|
||||
if messages[i].get("role") == "user":
|
||||
msg = messages[i]
|
||||
if msg.get("role") == "user" and not self._is_context_summary_content(
|
||||
msg.get("content")
|
||||
):
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
|
|
|||
|
|
@ -477,6 +477,51 @@ class TestCompactionRollupReproduction:
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestFindLastUserMessageIdxSkipsSummaryMarker:
|
||||
"""A context-compaction handoff banner is inserted with ``role="user"``
|
||||
when the head ends in an assistant/tool message (see the summary-role
|
||||
selection in ``compress``). ``_find_last_user_message_idx`` must NOT treat
|
||||
that banner as the latest user turn — otherwise, on a resumed or
|
||||
multi-compaction session, ``_ensure_last_user_message_in_tail`` anchors the
|
||||
tail to the summary and rolls the genuine last user message into the next
|
||||
compaction, re-triggering the active-task loss the anchor exists to prevent.
|
||||
(Salvaged from #36626 / issue #36624.)
|
||||
"""
|
||||
|
||||
def test_skips_user_role_context_summary_marker(self, compressor):
|
||||
from agent.context_compressor import SUMMARY_PREFIX
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "sys"},
|
||||
{"role": "user", "content": "REAL current task"},
|
||||
{"role": "assistant", "content": "working on it"},
|
||||
# A handoff summary re-inserted as a user-role message after resume.
|
||||
{"role": "user", "content": f"{SUMMARY_PREFIX}\n## Active Task\nold"},
|
||||
{"role": "assistant", "content": "continuing from the real task"},
|
||||
]
|
||||
# Latest *real* user message is index 1, not the summary at index 3.
|
||||
assert compressor._find_last_user_message_idx(messages, head_end=1) == 1
|
||||
|
||||
def test_returns_real_user_when_no_summary_present(self, compressor):
|
||||
messages = [
|
||||
{"role": "system", "content": "sys"},
|
||||
{"role": "user", "content": "first"},
|
||||
{"role": "assistant", "content": "reply"},
|
||||
{"role": "user", "content": "second"},
|
||||
]
|
||||
assert compressor._find_last_user_message_idx(messages, head_end=1) == 3
|
||||
|
||||
def test_all_user_messages_are_summaries_returns_minus_one(self, compressor):
|
||||
from agent.context_compressor import SUMMARY_PREFIX
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "sys"},
|
||||
{"role": "assistant", "content": "reply"},
|
||||
{"role": "user", "content": f"{SUMMARY_PREFIX}\nhandoff"},
|
||||
]
|
||||
assert compressor._find_last_user_message_idx(messages, head_end=1) == -1
|
||||
|
||||
|
||||
class TestSourceGuardrail:
|
||||
@pytest.fixture
|
||||
def source(self) -> str:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue