stage #4594 onto master (v0.51.550) — preserve visible transcript after manual /compress (#3133)

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 <franksong2702@users.noreply.github.com>
This commit is contained in:
nesquena-hermes committed 2026-06-21 06:46:47 +00:00
1 parent ccc000a911
commit 4bf9ff7518
4 files changed
+62 -14

No files matched your search

+2 -4
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
+7 -1
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',
@@ -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
+52 -8
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"},
]