Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 20 additions & 19 deletions agent/context_compressor.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,13 @@
"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. "
"If the latest user message is consistent with the '## Active Task' "
"section, you may use the summary as background. If the latest user "
"message contradicts, supersedes, changes topic from, or in any way "
"diverges from '## Active Task' / '## In Progress' / '## Pending User "
"Asks' / '## Remaining Work', the latest message WINS — discard those "
"stale items entirely and do not 'wrap up the old task first'. "
"If the latest user message is consistent with the historical summary, "
"you may use it as background. If the latest user message contradicts, "
"supersedes, changes topic from, or in any way diverges from the "
"'## Historical Task (prior session)' / '## Previous Work In Progress' / "
"'## Previously Pending User Asks' / '## Previous Work Remaining' sections, "
"the latest message WINS — discard those stale items entirely and do not "
"'wrap up the old task first'. "
Comment on lines +46 to +52
"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 "
Expand Down Expand Up @@ -1155,7 +1156,7 @@ def _bullets(items: list[str], limit: int = 8) -> str:
)

reason_text = f" Summary failure reason: {reason}." if reason else ""
body = f"""## Active Task
body = f"""## Historical Task (prior session)
{active_task}

## Goal
Expand All @@ -1172,7 +1173,7 @@ def _bullets(items: list[str], limit: int = 8) -> str:
## Active State
Unknown from deterministic fallback. Inspect current repository/session state if needed.

## In Progress
## Previous Work In Progress
{active_task}

## Blocked
Expand All @@ -1184,13 +1185,13 @@ def _bullets(items: list[str], limit: int = 8) -> str:
## Resolved Questions
None recoverable from deterministic fallback.

## Pending User Asks
## Previously Pending User Asks
{active_task}

## Relevant Files
{_bullets(relevant_files, limit=12)}

## Remaining Work
## Previous Work Remaining
Continue from the most recent unfulfilled user ask and protected tail messages. Verify state with tools before making claims.

## Last Dropped Turns
Expand Down Expand Up @@ -1312,9 +1313,9 @@ def _generate_summary(
_temporal_anchoring_rule = ""

# Shared structured template (used by both paths).
_template_sections = f"""## Active Task
_template_sections = f"""## Historical Task (prior session)
[THE SINGLE MOST IMPORTANT FIELD. Capture the user's most recent unfulfilled
input verbatim — the exact words they used. This includes:
input from the PRIOR session verbatim — the exact words they used. This includes:
Comment on lines +1316 to +1318
- Explicit task assignments ("refactor the auth module")
- Questions awaiting an answer ("waarom staat X op Y?", "wat zijn de volgende stappen?")
- Decisions awaiting input ("optie A of B?")
Expand Down Expand Up @@ -1359,8 +1360,8 @@ def _generate_summary(
- Any running processes or servers
- Environment details that matter]

## In Progress
[Work currently underway — what was being done when compaction fired]
## Previous Work In Progress
[Work currently underway in the prior session — what was being done when compaction fired]
Comment on lines +1363 to +1364

## Blocked
[Any blockers, errors, or issues not yet resolved. Include exact error messages.]
Expand All @@ -1371,14 +1372,14 @@ def _generate_summary(
## Resolved Questions
[Questions the user asked that were ALREADY answered — include the answer so it is not repeated]

## Pending User Asks
## Previously Pending User Asks
[Questions or requests from the user that have NOT yet been answered or fulfilled. If none, write "None."]

## Relevant Files
[Files read, modified, or created — with brief note on each]

## Remaining Work
[What remains to be done — framed as context, not instructions]
## Previous Work Remaining
[What remains to be done — framed as historical context, not instructions]
Comment on lines +1381 to +1382

## Critical Context
[Any specific values, error messages, configuration details, or data that would be lost without explicit preservation. NEVER include API keys, tokens, passwords, or credentials — write [REDACTED] instead.]
Expand All @@ -1399,7 +1400,7 @@ def _generate_summary(
NEW TURNS TO INCORPORATE:
{content_to_summarize}

Update the summary using this exact structure. PRESERVE all existing information that is still relevant. ADD new completed actions to the numbered list (continue numbering). Move items from "In Progress" to "Completed Actions" when done. Move answered questions to "Resolved Questions". Update "Active State" to reflect current state. Remove information only if it is clearly obsolete. CRITICAL: Update "## Active Task" to reflect the user's most recent unfulfilled input — this includes any question, decision request, or discussion turn that the assistant has not yet answered. Only write "None" if the last exchange was fully resolved.
Update the summary using this exact structure. PRESERVE all existing information that is still relevant. ADD new completed actions to the numbered list (continue numbering). Move items from "Previous Work In Progress" to "Completed Actions" when done. Move answered questions to "Resolved Questions". Update "Active State" to reflect current state. Remove information only if it is clearly obsolete. CRITICAL: Update "## Historical Task (prior session)" to reflect the user's most recent unfulfilled input from the prior session — this includes any question, decision request, or discussion turn that the assistant has not yet answered. Only write "None" if the last exchange was fully resolved.

{_template_sections}"""
else:
Expand Down Expand Up @@ -2116,7 +2117,7 @@ def compress(self, messages: List[Dict[str, Any]], current_tokens: int = None, f
_merge_summary_into_tail = True

# When the summary lands as a standalone role="user" message,
# weak models read the verbatim "## Active Task" quote of a past
# weak models read the verbatim "## Historical Task (prior session)" quote of a past
# user request as fresh input (#11475, #14521). Append the explicit
# end marker — the same one used in the merge-into-tail path — so
# the model has a clear "summary above, not new input" signal.
Expand Down
4 changes: 2 additions & 2 deletions tests/agent/test_context_compressor.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def test_summary_failure_uses_deterministic_fallback_with_recovered_context(self
result = c.compress(msgs)

combined = "\n".join(str(m.get("content", "")) for m in result)
assert "## Active Task" in combined
assert "## Historical Task (prior session)" in combined
assert "Please fix the compression summary failure" in combined
assert "read_file" in combined
assert "agent/context_compressor.py" in combined
Expand Down Expand Up @@ -1213,7 +1213,7 @@ def test_user_role_summary_carries_end_marker(self):
"""When the summary lands as standalone role='user' (e.g. head ends
with assistant/tool), the message body must include the explicit
'--- END OF CONTEXT SUMMARY ---' marker. Without it, weak models
read the verbatim past user request quoted in '## Active Task' as
read the verbatim past user request quoted in '## Historical Task (prior session)' as
fresh input (#11475, #14521).
"""
mock_response = MagicMock()
Expand Down
2 changes: 1 addition & 1 deletion tests/agent/test_context_compressor_temporal_anchoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def _boom():
prompt = mock_call.call_args.kwargs["messages"][0]["content"]
assert "TEMPORAL ANCHORING" not in prompt
# Structured template still intact.
assert "## Active Task" in prompt
assert "## Historical Task (prior session)" in prompt


def test_anchoring_rule_uses_date_from_hermes_time_now():
Expand Down
28 changes: 14 additions & 14 deletions tests/agent/test_resume_stale_active_task.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Regression coverage for #35344: a resumed session must not let a stale
``## Active Task`` from an inherited compaction handoff hijack the reply to a
"""Regression coverage for #35344 and #42812: a resumed session must not let a stale
``## Active Task`` (now renamed to ``## Historical Task (prior session)``) from an inherited compaction handoff hijack the reply to a
new, unrelated user message.

The failure mode (real report): a lineage was compacted, producing a handoff
Expand All @@ -12,12 +12,12 @@
named reverse-signal verbs. Two invariants guard the resume path specifically:

1. A handoff persisted under the OLD (conflicting) prefix is re-normalized to
the CURRENT prefix when it is re-compacted on a resumed lineage — so a
pre-fix stale handoff cannot keep its "resume exactly" directive forever.
the CURRENT prefix when it is re-compacted on a resumed lineage — so a
pre-fix stale handoff cannot keep its "resume exactly" directive forever.

2. The current handoff prefix contains an unambiguous "latest message wins /
discard stale Active Task" rule, so an unrelated new ask is privileged over
the inherited ``## Active Task``.
discard stale historical Task" rule, so an unrelated new ask is privileged over
the inherited ``## Historical Task (prior session)``.

These are content/structural assertions (no live model call) — they pin the
mechanism that makes the stale task historical rather than active.
Expand Down Expand Up @@ -46,12 +46,12 @@
)


def test_latest_message_wins_over_inherited_active_task():
def test_latest_message_wins_over_inherited_historical_task():
"""The handoff must explicitly privilege the latest user message over a
stale ``## Active Task`` — the core #35344 contract."""
stale ``## Historical Task (prior session)`` — the core #35344 contract."""
lower = SUMMARY_PREFIX.lower()
assert "latest user message" in lower
assert "## active task" in lower
assert "## historical task" in lower
# Conflict-resolution must be explicit, not implied.
assert "wins" in lower or "supersede" in lower
assert "discard" in lower
Expand All @@ -69,7 +69,7 @@ def test_resumed_stale_handoff_gets_renormalized_to_current_prefix():
prefix when re-normalized on re-compaction — so the "resume exactly"
directive cannot survive into a resumed session."""
stale_body = (
"## Active Task\n"
"## Historical Task (prior session)\n"
"User asked: 'Migrate the billing module to Stripe'\n\n"
"## Goal\nMigrate billing.\n"
)
Expand All @@ -92,7 +92,7 @@ def test_resumed_stale_handoff_gets_renormalized_to_current_prefix():
def test_legacy_prefix_handoff_also_renormalized():
"""The same upgrade applies to the oldest ``[CONTEXT SUMMARY]:`` handoff
format that may sit in a long-lived resumed lineage."""
legacy = f"{LEGACY_SUMMARY_PREFIX} ## Active Task\nUser asked: 'task A'"
legacy = f"{LEGACY_SUMMARY_PREFIX} ## Historical Task (prior session)\nUser asked: 'task A'"
renormalized = ContextCompressor._with_summary_prefix(legacy)
assert renormalized.startswith(SUMMARY_PREFIX)
assert LEGACY_SUMMARY_PREFIX not in renormalized
Expand All @@ -103,11 +103,11 @@ def test_inherited_handoff_detected_in_resumed_protected_head():
"""On a resumed lineage the handoff commonly sits right after the system
prompt (in the protected head). ``_find_latest_context_summary`` must
detect it there so re-compaction rehydrates state from it rather than
serializing it as a fresh user turn (which is what let the stale Active
serializing it as a fresh user turn (which is what let the stale Historical
Task read as live intent)."""
messages = [
{"role": "system", "content": "system prompt"},
{"role": "user", "content": f"{SUMMARY_PREFIX}\n## Active Task\nUser asked: 'task A'"},
{"role": "user", "content": f"{SUMMARY_PREFIX}\n## Historical Task (prior session)\nUser asked: 'task A'"},
{"role": "assistant", "content": "ok"},
{"role": "user", "content": "Unrelated task B: what's the capital of France?"},
]
Expand All @@ -129,7 +129,7 @@ def test_historical_prefixed_handoff_detected_and_stripped():
stale 'resume exactly' text as a fresh turn."""
messages = [
{"role": "system", "content": "system prompt"},
{"role": "user", "content": f"{_OLD_CONFLICTING_PREFIX}\n## Active Task\nUser asked: 'task A'"},
{"role": "user", "content": f"{_OLD_CONFLICTING_PREFIX}\n## Historical Task (prior session)\nUser asked: 'task A'"},
{"role": "assistant", "content": "ok"},
{"role": "user", "content": "Unrelated task B"},
]
Expand Down