Merge pull request #4602 from nesquena/stage-4594

Release v0.51.551 — manual /compress preserves visible transcript (#4594/#3133)
This commit is contained in:
nesquena-hermes 2026-06-20 23:58:40 -07:00 committed by GitHub
commit cf16ad4bb5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 68 additions and 14 deletions

View file

@ -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

View file

@ -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

View file

@ -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',

View file

@ -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

View file

@ -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"},
]