diff --git a/tests/tools/test_session_search.py b/tests/tools/test_session_search.py index 4e50a77b0cc2c..d644ae4b9ae9f 100644 --- a/tests/tools/test_session_search.py +++ b/tests/tools/test_session_search.py @@ -272,6 +272,19 @@ class TestReadShape: assert len(result["messages"]) == 5 assert result["session_meta"]["title"] == "Building the Modpack" + def test_read_strips_ansi_sequences_from_messages(self, db): + db.create_session("s_ansi", source="cli") + db.append_message("s_ansi", role="user", content="plain") + db.append_message( + "s_ansi", role="assistant", content="\u001b[31mred text\u001b[0m and more" + ) + db._conn.commit() + result = json.loads(session_search(session_id="s_ansi", db=db)) + assert result["success"] is True + rendered = [m["content"] for m in result["messages"] if m.get("content")] + assert any(text == "red text and more" for text in rendered) + assert all("\u001b" not in text for text in rendered) + def test_read_truncates_large_session(self, db): db.create_session("s_big", source="cli") for i in range(50): diff --git a/tools/session_search_tool.py b/tools/session_search_tool.py index be063a83e40bb..f25debc0d7075 100644 --- a/tools/session_search_tool.py +++ b/tools/session_search_tool.py @@ -247,6 +247,12 @@ def _shape_message( is added so callers know the payload was bounded. """ raw_content = m.get("content") + if isinstance(raw_content, str) and "\x1b" in raw_content: + # Recalled messages can carry ANSI escape sequences (e.g. archived + # terminal output). Strip them before returning content to the model. + from tools.ansi_strip import strip_ansi + + raw_content = strip_ansi(raw_content) if max_content_len and raw_content and len(raw_content) > max_content_len: content = raw_content[:max_content_len] + "…" truncated = True