diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a080547d9..d98746c7a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ## [Unreleased] +### Fixed + +- Prevent compression-summary cards from using ordinary tool output that merely mentions context compression. The streaming path now reuses the shared strict compression-marker predicate, so only synthetic marker prefixes from non-tool messages can seed `compression_anchor_summary`; skill/tool JSON and user discussion about compaction no longer render as misleading `Context compaction` reference cards. + ## [v0.51.118] — 2026-05-22 — Release CP (stage-pr2773 — 1-PR hotfix — v0.51.117 brick fix: chat input restored) ### Fixed diff --git a/api/compression_anchor.py b/api/compression_anchor.py index f251851c4f..12d964158b 100644 --- a/api/compression_anchor.py +++ b/api/compression_anchor.py @@ -53,7 +53,7 @@ def _content_has_part_type(content, part_types): ) -def _is_context_compression_marker(message): +def is_context_compression_marker(message): """Return true for synthetic compression/reference cards, not user turns.""" if not isinstance(message, dict): return False @@ -71,6 +71,11 @@ def _is_context_compression_marker(message): ) +def _is_context_compression_marker(message): + """Backward-compatible alias for callers that have not switched yet.""" + return is_context_compression_marker(message) + + def visible_messages_for_anchor(messages, *, auto_compression: bool = False): """Return transcript messages that can anchor compression UI metadata. diff --git a/api/streaming.py b/api/streaming.py index 663d84d174..5718ee1c8f 100644 --- a/api/streaming.py +++ b/api/streaming.py @@ -35,7 +35,7 @@ load_settings, ) from api.helpers import redact_session_data, _redact_text -from api.compression_anchor import visible_messages_for_anchor +from api.compression_anchor import is_context_compression_marker, visible_messages_for_anchor from api.metering import meter from api.run_journal import RunJournalWriter from api.turn_journal import append_turn_journal_event_for_stream @@ -2299,15 +2299,7 @@ def _dedupe_replayed_active_context(previous_context, result_messages): def _is_context_compression_marker(msg): - if not isinstance(msg, dict): - return False - text = _message_text(msg.get('content', '')).lower() - return ( - 'context compaction' in text - or 'context compression' in text - or 'context was auto-compressed' in text - or 'active task list was preserved across context compression' in text - ) + return is_context_compression_marker(msg) def _compact_summary_text(raw_text: str | None, limit: int = 320) -> str | None: diff --git a/tests/test_issue2028_compression_anchor_helpers.py b/tests/test_issue2028_compression_anchor_helpers.py index 1fcb4f6ad8..d675d2a004 100644 --- a/tests/test_issue2028_compression_anchor_helpers.py +++ b/tests/test_issue2028_compression_anchor_helpers.py @@ -4,7 +4,8 @@ from pathlib import Path -from api.compression_anchor import visible_messages_for_anchor +from api.compression_anchor import is_context_compression_marker, visible_messages_for_anchor +from api.streaming import _compression_summary_from_messages, _is_context_compression_marker def test_legacy_duplicate_anchor_helpers_are_removed(): @@ -57,3 +58,42 @@ def test_visible_messages_for_anchor_keeps_manual_user_messages_simple(): [user_tool_metadata, user_attachment, assistant_tool_metadata], auto_compression=True, ) == [user_tool_metadata, user_attachment, assistant_tool_metadata] + + +def test_context_compression_marker_detection_is_prefix_and_role_scoped(): + real_marker = { + "role": "assistant", + "content": "[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted.", + } + preserved_tasks_marker = { + "role": "user", + "content": "[Your active task list was preserved across context compression] - [ ] follow up", + } + tool_noise = { + "role": "tool", + "content": "{\"description\": \"Troubleshoot frequent context compression indicators\"}", + } + user_discussion = { + "role": "user", + "content": "Why do I see context compression after every message?", + } + + assert is_context_compression_marker(real_marker) + assert is_context_compression_marker(preserved_tasks_marker) + assert _is_context_compression_marker(real_marker) + assert not is_context_compression_marker(tool_noise) + assert not is_context_compression_marker(user_discussion) + + +def test_compression_summary_ignores_tool_output_that_mentions_compression(): + marker = { + "role": "assistant", + "content": "[CONTEXT COMPACTION — REFERENCE ONLY] Keep this handoff as reference.", + } + skill_tool_output = { + "role": "tool", + "content": "{\"name\": \"hermes-webui-operations\", \"content\": \"Troubleshooting frequent context compression indicators...\"}", + } + + assert _compression_summary_from_messages([marker, skill_tool_output]) == marker["content"] + assert _compression_summary_from_messages([skill_tool_output]) is None