From 8cf9e8a61bf698fe6ebde5c65de1f7e40af7e2af Mon Sep 17 00:00:00 2001 From: fangliquanflq Date: Mon, 10 Aug 2026 23:26:56 +0800 Subject: [PATCH] fix(compression): ignore background process notifications --- agent/context_compressor.py | 6 ++- ...context_compressor_zero_user_provenance.py | 47 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/agent/context_compressor.py b/agent/context_compressor.py index 3eb05b6b97..e5ceb59df5 100644 --- a/agent/context_compressor.py +++ b/agent/context_compressor.py @@ -177,6 +177,7 @@ MAX_ITERATIONS_SUMMARY_REQUEST = ( "Please provide a final response summarizing what you've found and accomplished so far, " "without calling any more tools." ) +_BACKGROUND_PROCESS_NOTIFICATION_PREFIX = "[IMPORTANT: Background process " def _fresh_compaction_message_copy(msg: Dict[str, Any]) -> Dict[str, Any]: @@ -4582,7 +4583,8 @@ This compaction should PRIORITISE preserving all information related to the focu """Recognize internal user-role rows after SessionDB projection. SessionDB preserves role/content but not underscore-prefixed metadata, - so the stable todo and continuation content markers are authoritative. + so stable runtime-notification, todo, and continuation content markers + are authoritative. """ if not isinstance(message, dict) or message.get("role") != "user": return False @@ -4618,6 +4620,8 @@ This compaction should PRIORITISE preserving all information related to the focu _LENGTH_CONTINUATION_NETWORK_STUB, _LENGTH_CONTINUATION_OUTPUT_LIMIT, } or text.startswith( + _BACKGROUND_PROCESS_NOTIFICATION_PREFIX + ) or text.startswith( TODO_INJECTION_HEADER + "\n" ) or text.startswith( _LENGTH_CONTINUATION_DROPPED_TOOLS_PREFIX diff --git a/tests/agent/test_context_compressor_zero_user_provenance.py b/tests/agent/test_context_compressor_zero_user_provenance.py index 6890eba868..7a5bd12568 100644 --- a/tests/agent/test_context_compressor_zero_user_provenance.py +++ b/tests/agent/test_context_compressor_zero_user_provenance.py @@ -21,6 +21,7 @@ from agent.conversation_compression import ( compress_context, ) from hermes_state import SessionDB +from tools.process_registry import format_process_notification from tools.todo_tool import TODO_INJECTION_HEADER @@ -237,6 +238,52 @@ def test_real_task_wins_over_trailing_max_iterations_nudge(compressor): assert messages[idx]["content"] == human["content"] +@pytest.mark.parametrize( + "event", + [ + pytest.param( + { + "type": "completion", + "session_id": "proc_build", + "command": "scripts/run_tests.sh tests/agent/", + "exit_code": 0, + "output": "42 passed", + }, + id="completion", + ), + pytest.param( + { + "type": "watch_match", + "session_id": "proc_server", + "command": "python server.py", + "pattern": "Application startup complete", + "output": "Application startup complete", + }, + id="watch_match", + ), + ], +) +def test_background_process_notifications_do_not_become_compaction_anchors( + compressor, event +): + notification = format_process_notification(event) + assert notification is not None + process_turn = {"role": "user", "content": notification} + human = {"role": "user", "content": "Refactor the auth module and add tests."} + messages = [ + human, + {"role": "assistant", "content": "Working on it."}, + process_turn, + ] + + assert ContextCompressor._is_synthetic_compression_user_turn(process_turn) is True + assert ContextCompressor._transcript_has_real_user_turn([process_turn]) is False + assert compressor._derive_auto_focus_topic(messages) == ( + "Recent user focus:\n- Refactor the auth module and add tests." + ) + assert compressor._find_last_user_message_idx(messages, head_end=0) == 0 + + @pytest.mark.parametrize( "content", [