From 4bf9ff7518c707c19069083c006af67ed4dae32b Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Sun, 21 Jun 2026 06:46:47 +0000 Subject: [PATCH 1/2] =?UTF-8?q?stage=20#4594=20onto=20master=20(v0.51.550)?= =?UTF-8?q?=20=E2=80=94=20preserve=20visible=20transcript=20after=20manual?= =?UTF-8?q?=20/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"}, ] From d281b7cf63830e50e5c19e30f7de84a379e5d465 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Sun, 21 Jun 2026 06:54:48 +0000 Subject: [PATCH 2/2] =?UTF-8?q?docs(release):=20stamp=20v0.51.551=20?= =?UTF-8?q?=E2=80=94=20manual=20/compress=20preserves=20visible=20transcri?= =?UTF-8?q?pt=20(#4594/#3133)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f20d7fe..77ac862d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.551] — 2026-06-21 — Release TJ (manual /compress keeps the visible conversation) + +### Fixed + +- **Manually compressing a session no longer makes earlier messages disappear from the conversation window (#3133).** The manual `/compress` action was overwriting the visible transcript with the reduced model-facing context (and clearing settled tool history), so messages vanished on screen and after reload even though only the model's context should shrink. It now preserves the visible transcript and tool history and prunes only the model-facing context payload — matching how automatic compression already behaves. Thanks @franksong2702. + ## [v0.51.550] — 2026-06-21 — Release TI (Transparent Stream keeps its activity after a turn settles or reloads) ### Fixed