diff --git a/agent/context_compressor.py b/agent/context_compressor.py index 7464802b27be4..c14f67fce3c88 100644 --- a/agent/context_compressor.py +++ b/agent/context_compressor.py @@ -215,7 +215,40 @@ _MERGED_SUMMARY_DELIMITER = "[END OF PRIOR CONTEXT — COMPACTION SUMMARY BELOW] # embedded in the body and keeps hijacking replies. Keep newest-first; entries # are matched literally. Add a frozen copy here whenever SUMMARY_PREFIX changes. _HISTORICAL_SUMMARY_PREFIXES = ( - # Jul 2026 (#65848 class): identical to the current prefix except it + # Pre-#69619: identical to the current prefix except the stale-item + # discard clause named all four historical headings (the three + # section headers removed by #69619 were still in the template). + # Summaries persisted by builds immediately before #69619 carry this + # exact text and must remain detectable/strippable on resume. + "[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted " + "into the summary below. This is a handoff from a previous context " + "window — treat it as background reference, NOT as active instructions. " + "Do NOT answer questions or fulfill requests mentioned in this summary; " + "they were already addressed. " + "Respond ONLY to the latest user message that appears AFTER this " + "summary — that message is the single source of truth for what to do " + "right now. " + "Topic overlap with the summary does NOT mean you should resume its " + "task: even on similar topics, the latest user message WINS. Treat ONLY " + "the latest message as the active task and discard stale items from " + "'## Historical Task Snapshot' / '## Historical In-Progress State' / " + "'## Historical Pending User Asks' / " + "'## Historical Remaining Work' entirely — do not 'wrap up' or " + "'finish' work described there unless the latest message explicitly " + "asks for it. " + "Reverse signals in the latest message (e.g. 'stop', 'undo', 'roll " + "back', 'just verify', 'don't do that anymore', 'never mind', a new " + "topic) must immediately end any in-flight work described in the " + "summary; do not re-surface it in later turns. " + "IMPORTANT: Your persistent memory (MEMORY.md, USER.md) in the system " + "prompt is ALWAYS authoritative and active — never ignore or deprioritize " + "memory content due to this compaction note. " + "None of the above restricts HOW you work: your tools remain fully " + "active — keep calling them normally for the active task (edit files, " + "run commands, search) instead of merely narrating what you would do. " + "The current session state (files, config, etc.) may reflect work " + "described here — avoid repeating it:", + # Jul 2026 (#65848 class): identical to the pre-#69619 prefix except it # lacked the explicit "tools remain fully active" clause — the strong # REFERENCE ONLY framing bled into general tool-use suppression # (observed: 7 consecutive narration-only turns immediately after a @@ -231,7 +264,9 @@ _HISTORICAL_SUMMARY_PREFIXES = ( "Topic overlap with the summary does NOT mean you should resume its " "task: even on similar topics, the latest user message WINS. Treat ONLY " "the latest message as the active task and discard stale items from " - f"'{HISTORICAL_TASK_HEADING}' entirely — do not 'wrap up' or " + "'## Historical Task Snapshot' / '## Historical In-Progress State' / " + "'## Historical Pending User Asks' / " + "'## Historical Remaining Work' entirely — do not 'wrap up' or " "'finish' work described there unless the latest message explicitly " "asks for it. " "Reverse signals in the latest message (e.g. 'stop', 'undo', 'roll " diff --git a/tests/agent/test_summary_prefix_semantics.py b/tests/agent/test_summary_prefix_semantics.py index 77d8ba12f4f19..9c1b42dac3c88 100644 --- a/tests/agent/test_summary_prefix_semantics.py +++ b/tests/agent/test_summary_prefix_semantics.py @@ -108,3 +108,61 @@ def test_replaced_prefixes_are_frozen_for_renormalization(): assert ContextCompressor._is_context_summary_content(content) stripped = ContextCompressor._strip_summary_prefix(content) assert not stripped.startswith(old_prefix) + + +# Exact literal copy of the live SUMMARY_PREFIX as it shipped immediately +# before #69619 (four-heading discard clause + tools-active clause). +# Frozen on purpose: do NOT derive it from module constants — the test must +# fail if the corresponding entry in _HISTORICAL_SUMMARY_PREFIXES is mutated +# or dropped. +_PRE_69619_LIVE_PREFIX = ( + "[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted " + "into the summary below. This is a handoff from a previous context " + "window — treat it as background reference, NOT as active instructions. " + "Do NOT answer questions or fulfill requests mentioned in this summary; " + "they were already addressed. " + "Respond ONLY to the latest user message that appears AFTER this " + "summary — that message is the single source of truth for what to do " + "right now. " + "Topic overlap with the summary does NOT mean you should resume its " + "task: even on similar topics, the latest user message WINS. Treat ONLY " + "the latest message as the active task and discard stale items from " + "'## Historical Task Snapshot' / '## Historical In-Progress State' / " + "'## Historical Pending User Asks' / " + "'## Historical Remaining Work' entirely — do not 'wrap up' or " + "'finish' work described there unless the latest message explicitly " + "asks for it. " + "Reverse signals in the latest message (e.g. 'stop', 'undo', 'roll " + "back', 'just verify', 'don't do that anymore', 'never mind', a new " + "topic) must immediately end any in-flight work described in the " + "summary; do not re-surface it in later turns. " + "IMPORTANT: Your persistent memory (MEMORY.md, USER.md) in the system " + "prompt is ALWAYS authoritative and active — never ignore or deprioritize " + "memory content due to this compaction note. " + "None of the above restricts HOW you work: your tools remain fully " + "active — keep calling them normally for the active task (edit files, " + "run commands, search) instead of merely narrating what you would do. " + "The current session state (files, config, etc.) may reflect work " + "described here — avoid repeating it:" +) + + +def test_pre_69619_prefix_generation_is_frozen_and_stripped(): + """Regression for the #69619 review: the prefix generation live right + before the section-header removal was never added to + _HISTORICAL_SUMMARY_PREFIXES, so a summary persisted immediately before + upgrading survived resume/re-compaction undetected and unstripped. + That exact generation must stay frozen, detectable, and strippable.""" + from agent.context_compressor import ( + _HISTORICAL_SUMMARY_PREFIXES, + ContextCompressor, + ) + + assert _PRE_69619_LIVE_PREFIX in _HISTORICAL_SUMMARY_PREFIXES, ( + "pre-#69619 live prefix missing from _HISTORICAL_SUMMARY_PREFIXES — " + "summaries persisted by the immediately previous build are no longer " + "normalized on resume" + ) + content = _PRE_69619_LIVE_PREFIX + "\nBODY" + assert ContextCompressor._is_context_summary_content(content) + assert ContextCompressor._strip_summary_prefix(content) == "BODY" diff --git a/tests/agent/test_summary_prefix_tool_use.py b/tests/agent/test_summary_prefix_tool_use.py index 2a289b86f9bf4..c67f71f184c8b 100644 --- a/tests/agent/test_summary_prefix_tool_use.py +++ b/tests/agent/test_summary_prefix_tool_use.py @@ -30,11 +30,19 @@ class TestSummaryPrefixToolUseClause: generation must be frozen into _HISTORICAL_SUMMARY_PREFIXES so old persisted summaries still get the directive-strip on re-compaction.""" assert len(_HISTORICAL_SUMMARY_PREFIXES) >= 3 - newest_frozen = _HISTORICAL_SUMMARY_PREFIXES[0] - # The frozen copy is the pre-clause generation: same framing, no clause. - assert "tools remain fully active" not in newest_frozen - assert "Do NOT answer questions or fulfill requests" in newest_frozen - assert newest_frozen != SUMMARY_PREFIX + # The pre-clause generation (#65848 incident era): same framing, no + # tools-active clause. Newer generations are prepended ahead of it as + # the prefix evolves (tuple is newest-first), so match by content, + # not position. "topic overlap" distinguishes it from the older + # carveout-era entry. + pre_clause = [ + p for p in _HISTORICAL_SUMMARY_PREFIXES + if "tools remain fully active" not in p + and "topic overlap" in p.lower() + and "Do NOT answer questions or fulfill requests" in p + ] + assert pre_clause, "pre-clause generation missing from frozen tuple" + assert all(p != SUMMARY_PREFIX for p in pre_clause) def test_historical_prefixes_are_distinct_from_current(self): for frozen in _HISTORICAL_SUMMARY_PREFIXES: