fix(compaction): freeze pre-change SUMMARY_PREFIX generation, restore mutated entry
Address review on #69619: the previous commit mutated the newest frozen entry in _HISTORICAL_SUMMARY_PREFIXES and never froze the live prefix it retired (the generation with both the four-heading discard clause and the tools-active clause). A summary persisted immediately before upgrading was therefore treated as an ordinary message on resume/re-compaction, keeping the old handoff text embedded in the body. - Prepend the exact pre-change live prefix as a new frozen entry (newest-first), leaving all existing frozen entries byte-identical - Restore the Jul 2026 (#65848 class) frozen entry to its original four-heading text - Pin the retired generation as a literal in test_summary_prefix_semantics.py so mutating or dropping it fails CI - Make the #65848 tool-use regression position-agnostic (match the pre-clause generation by content, not tuple index) Verified byte-identity of both rescued generations against the parent commit. 233 focused prefix/resume/compressor tests pass.
This commit is contained in:
parent
b59cce9178
commit
8204b27618
|
|
@ -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 "
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue