From 4bf9ff7518c707c19069083c006af67ed4dae32b Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Sun, 21 Jun 2026 06:46:47 +0000 Subject: [PATCH] =?UTF-8?q?stage=20#4594=20onto=20master=20(v0.51.550)=20?= =?UTF-8?q?=E2=80=94=20preserve=20visible=20transcript=20after=20manual=20?= =?UTF-8?q?/compress=20(#3133)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Manual /compress wrote the compressed payload into BOTH s.messages (visible) and s.context_messages (model-facing) + wiped s.tool_calls -> messages vanished from the window. Fix keeps s.messages + tool history durable, deep-copies compressed only into s.context_messages, anchors off visible msgs. Co-authored-by: franksong2702 --- api/routes.py | 6 +- tests/test_gateway_sync.py | 8 ++- ...st_issue2028_compression_anchor_helpers.py | 2 +- tests/test_sprint46.py | 60 ++++++++++++++++--- 4 files changed, 62 insertions(+), 14 deletions(-) diff --git a/api/routes.py b/api/routes.py index 7de354e9..bce86d29 100644 --- a/api/routes.py +++ b/api/routes.py @@ -18397,15 +18397,13 @@ def _handle_session_compress(handler, body): if _sanitize_messages_for_api(s.messages) != original_messages: return bad(handler, "Session was modified during compression; please retry.", 409) - s.messages = compressed - s.context_messages = compressed - s.tool_calls = [] + s.context_messages = copy.deepcopy(compressed) s.active_stream_id = None s.pending_user_message = None s.pending_attachments = [] s.pending_started_at = None s.pending_user_source = None - visible_after = visible_messages_for_anchor(compressed, auto_compression=False) + visible_after = visible_messages_for_anchor(s.messages, auto_compression=False) s.compression_anchor_visible_idx = max(0, len(visible_after) - 1) if visible_after else None s.compression_anchor_message_key = _anchor_message_key(visible_after[-1]) if visible_after else None summary_text = None diff --git a/tests/test_gateway_sync.py b/tests/test_gateway_sync.py index b4836990..1437430b 100644 --- a/tests/test_gateway_sync.py +++ b/tests/test_gateway_sync.py @@ -1331,7 +1331,13 @@ def test_sessions_response_backfills_imported_messaging_source_metadata(cleanup_ sid = 'gw_legacy_import_weixin_001' cleanup_test_sessions.append(sid) try: - _insert_gateway_session(conn, session_id=sid, source='weixin', title='Weixin Session') + _insert_gateway_session( + conn, + session_id=sid, + source='weixin', + title='Weixin Session', + session_key='agent:test:weixin:legacy-import-backfill', + ) s = Session( session_id=sid, title='Legacy Imported Weixin', diff --git a/tests/test_issue2028_compression_anchor_helpers.py b/tests/test_issue2028_compression_anchor_helpers.py index 35334885..aa679492 100644 --- a/tests/test_issue2028_compression_anchor_helpers.py +++ b/tests/test_issue2028_compression_anchor_helpers.py @@ -14,7 +14,7 @@ def test_legacy_duplicate_anchor_helpers_are_removed(): assert "def _visible_messages_for_anchor" not in routes_src assert "def _visible_messages_for_compression_anchor" not in streaming_src - assert "visible_messages_for_anchor(compressed, auto_compression=False)" in routes_src + assert "visible_messages_for_anchor(s.messages, auto_compression=False)" in routes_src assert "visible_messages_for_anchor(s.messages, auto_compression=True)" in streaming_src diff --git a/tests/test_sprint46.py b/tests/test_sprint46.py index 1b0856a5..7fd1c25e 100644 --- a/tests/test_sprint46.py +++ b/tests/test_sprint46.py @@ -102,7 +102,7 @@ def _install_fake_compression_runtime(monkeypatch, agent_cls): ) -def _make_session(messages=None): +def _make_session(messages=None, tool_calls=None): SESSION_DIR.mkdir(parents=True, exist_ok=True) messages = messages or [ {"role": "user", "content": "one"}, @@ -116,6 +116,7 @@ def _make_session(messages=None): workspace="/tmp/hermes-webui-test", model="openai/gpt-5.4-mini", messages=messages, + tool_calls=tool_calls or [], ) s.save(touch_updated_at=False) return s.session_id @@ -130,7 +131,23 @@ def test_session_compress_requires_session_id(cleanup_test_sessions): def test_session_compress_roundtrip(monkeypatch, cleanup_test_sessions): created = cleanup_test_sessions - sid = _make_session() + original_messages = [ + {"role": "user", "content": "one"}, + {"role": "assistant", "content": "two"}, + {"role": "user", "content": "three"}, + {"role": "assistant", "content": "four"}, + ] + compressed_messages = [original_messages[0], original_messages[-1]] + settled_tool_calls = [ + { + "id": "call_1", + "name": "terminal", + "assistant_msg_idx": 1, + "done": True, + "result": "schema.sql", + } + ] + sid = _make_session(original_messages, tool_calls=settled_tool_calls) created.append(sid) _install_fake_compression_runtime(monkeypatch, _FakeAgent) @@ -144,18 +161,27 @@ def test_session_compress_roundtrip(monkeypatch, cleanup_test_sessions): assert payload["focus_topic"] == "database schema" assert payload["summary"]["headline"] == "Compressed: 4 → 2 messages" assert payload["session"]["session_id"] == sid - assert payload["session"]["messages"] == [ - {"role": "user", "content": "one"}, - {"role": "assistant", "content": "four"}, - ] + assert payload["session"]["messages"] == original_messages + assert payload["session"]["tool_calls"] == settled_tool_calls + assert payload["session"]["message_count"] == len(original_messages) assert payload["session"]["compression_anchor_summary"] is not None - assert payload["session"]["compression_anchor_visible_idx"] == 1 + assert payload["session"]["compression_anchor_visible_idx"] == 3 assert isinstance(payload["session"]["compression_anchor_message_key"], dict) assert payload["session"]["compression_anchor_message_key"].get("role") == "assistant" + assert payload["session"]["compression_anchor_message_key"].get("text") == "four" loaded = get_session(sid) + persisted = Session.load(sid) + assert loaded.messages == original_messages + assert loaded.context_messages == compressed_messages + assert loaded.tool_calls == settled_tool_calls + assert persisted.messages == original_messages + assert persisted.context_messages == compressed_messages + assert persisted.tool_calls == settled_tool_calls assert loaded.compression_anchor_summary == payload["session"]["compression_anchor_summary"] assert loaded.compression_anchor_visible_idx == payload["session"]["compression_anchor_visible_idx"] assert loaded.compression_anchor_message_key == payload["session"]["compression_anchor_message_key"] + assert persisted.compression_anchor_visible_idx == 3 + assert persisted.compression_anchor_message_key == payload["session"]["compression_anchor_message_key"] assert _FakeAgent.last_instance is not None assert _FakeAgent.last_instance.context_compressor.calls[0]["focus_topic"] == "database schema" @@ -185,7 +211,16 @@ def test_session_compress_start_is_async_and_reuses_running_job(monkeypatch, cle self.instances.append(self) created = cleanup_test_sessions - sid = _make_session() + settled_tool_calls = [ + { + "id": "call_async_1", + "name": "read_file", + "assistant_msg_idx": 1, + "done": True, + "result": "config.yaml", + } + ] + sid = _make_session(tool_calls=settled_tool_calls) created.append(sid) _install_fake_compression_runtime(monkeypatch, BlockingAgent) try: @@ -226,6 +261,15 @@ def test_session_compress_start_is_async_and_reuses_running_job(monkeypatch, cle assert done_payload is not None assert done_payload["summary"]["headline"] == "Compressed: 4 → 2 messages" assert done_payload["session"]["messages"] == [ + {"role": "user", "content": "one"}, + {"role": "assistant", "content": "two"}, + {"role": "user", "content": "three"}, + {"role": "assistant", "content": "four"}, + ] + assert done_payload["session"]["tool_calls"] == settled_tool_calls + persisted = Session.load(sid) + assert persisted.tool_calls == settled_tool_calls + assert persisted.context_messages == [ {"role": "user", "content": "one"}, {"role": "assistant", "content": "four"}, ]