From 286ecd26d8770639a15e3c448cc4405ab5da6362 Mon Sep 17 00:00:00 2001 From: Tranquil-Flow Date: Fri, 24 Apr 2026 20:46:30 +1000 Subject: [PATCH] fix(agent): strip MEDIA directives from compressor summarizer input (#14665) --- agent/context_compressor.py | 6 ++ .../agent/test_compressor_media_stripping.py | 56 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 tests/agent/test_compressor_media_stripping.py diff --git a/agent/context_compressor.py b/agent/context_compressor.py index f83c2fba7c..c2f6fa3d24 100644 --- a/agent/context_compressor.py +++ b/agent/context_compressor.py @@ -1006,10 +1006,16 @@ class ContextCompressor(ContextEngine): (API keys, tokens, passwords) from leaking into the summary that gets sent to the auxiliary model and persisted across compactions. """ + # Strip MEDIA directives before sending to the summarizer — if they + # leak into the summary, the downstream model may re-emit them as + # active directives on the next turn (#14665). + _MEDIA_RE = re.compile(r'MEDIA:\S+') + parts = [] for msg in turns: role = msg.get("role", "unknown") content = redact_sensitive_text(msg.get("content") or "") + content = _MEDIA_RE.sub("[media attachment]", content) # Tool results: keep enough content for the summarizer if role == "tool": diff --git a/tests/agent/test_compressor_media_stripping.py b/tests/agent/test_compressor_media_stripping.py new file mode 100644 index 0000000000..f995ac98a0 --- /dev/null +++ b/tests/agent/test_compressor_media_stripping.py @@ -0,0 +1,56 @@ +"""Tests for MEDIA directive stripping in context compaction (#14665). + +MEDIA directives in assistant messages must not leak into compaction +summaries — if they do, the downstream model re-emits them as active +directives on the next turn. +""" +import pytest +from unittest.mock import patch +from agent.context_compressor import ContextCompressor + + +@pytest.fixture() +def compressor(): + with patch("agent.context_compressor.get_model_context_length", return_value=100000): + return ContextCompressor( + model="test/model", + threshold_percent=0.85, + protect_first_n=2, + protect_last_n=2, + quiet_mode=True, + ) + + +class TestMediaDirectiveStripping: + """MEDIA directives must be stripped before summarization (#14665).""" + + def test_media_directive_stripped_from_assistant(self, compressor): + turns = [ + {"role": "assistant", "content": "Here is the audio MEDIA:/tmp/voice.ogg done."}, + ] + result = compressor._serialize_for_summary(turns) + assert "MEDIA:/tmp/voice.ogg" not in result + assert "[media attachment]" in result + + def test_media_directive_stripped_from_tool_result(self, compressor): + turns = [ + {"role": "tool", "tool_call_id": "t1", "content": "Generated MEDIA:/tmp/out.mp3 successfully"}, + ] + result = compressor._serialize_for_summary(turns) + assert "MEDIA:/tmp/out.mp3" not in result + assert "[media attachment]" in result + + def test_non_media_content_preserved(self, compressor): + turns = [ + {"role": "assistant", "content": "The file path is /tmp/test.txt and it works."}, + ] + result = compressor._serialize_for_summary(turns) + assert "/tmp/test.txt" in result + + def test_multiple_media_directives(self, compressor): + turns = [ + {"role": "assistant", "content": "MEDIA:/a.ogg and MEDIA:/b.mp3"}, + ] + result = compressor._serialize_for_summary(turns) + assert "MEDIA:" not in result + assert result.count("[media attachment]") == 2