diff --git a/CHANGELOG.md b/CHANGELOG.md index 33ed7ed6..a4838fb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ## [Unreleased] +### Fixed + +- Messaging/session display merges now preserve sidecar transcript order when the sidecar already contains at least as many rows as the mirrored state store, avoiding role/content fallback sorting when timestamp precision collapses. + ## [v0.51.153] — 2026-05-28 — Release DY (stage-batch35 — 11-PR low-risk cleanup: title-language + clarify SSE + upload filename + discoverability + SSE reconnect + gateway image + docker docs) ### Changed diff --git a/api/routes.py b/api/routes.py index 8ee764df..10e5a9b3 100644 --- a/api/routes.py +++ b/api/routes.py @@ -2223,6 +2223,12 @@ def _merged_session_messages_for_display(session, cli_messages=None) -> list: sidecar_messages = list(getattr(session, "messages", []) or []) if cli_messages: if sidecar_messages and sidecar_messages != cli_messages: + if len(sidecar_messages) >= len(cli_messages): + return merge_session_messages_append_only( + sidecar_messages, + cli_messages, + truncation_watermark=getattr(session, "truncation_watermark", None), + ) merged_messages = [] seen_message_keys = set() for msg in sorted(list(cli_messages) + list(sidecar_messages), key=lambda m: ( diff --git a/tests/test_issue2472_fork_from_here_messaging.py b/tests/test_issue2472_fork_from_here_messaging.py index c82ea764..44d00e66 100644 --- a/tests/test_issue2472_fork_from_here_messaging.py +++ b/tests/test_issue2472_fork_from_here_messaging.py @@ -66,6 +66,39 @@ def test_messaging_merge_helper_dedupes_equivalent_timestamp_formats(): assert [m["content"] for m in merged] == ["hi", "same answer"] +def test_messaging_merge_preserves_longer_sidecar_order_when_timestamps_collapse(): + """A repaired messaging sidecar can preserve order but lose subsecond timestamps. + + Re-sorting those messages by ``(timestamp, role, content)`` groups assistant + and tool rows before user rows, making the WebUI look like replies vanished. + """ + session = SimpleNamespace( + messages=[ + {"role": "assistant", "content": "prior answer", "timestamp": 100.0}, + {"role": "user", "content": "first prompt", "timestamp": 101.0}, + {"role": "assistant", "content": "first answer", "timestamp": 101.0}, + {"role": "user", "content": "second prompt", "timestamp": 101.0}, + {"role": "assistant", "content": "second answer", "timestamp": 101.0}, + ] + ) + cli_messages = [ + {"role": "user", "content": "first prompt", "timestamp": 101.1}, + {"role": "assistant", "content": "first answer", "timestamp": 101.2}, + {"role": "user", "content": "second prompt", "timestamp": 101.3}, + {"role": "assistant", "content": "second answer", "timestamp": 101.4}, + ] + + merged = routes._merged_session_messages_for_display(session, cli_messages) + + assert [m["content"] for m in merged] == [ + "prior answer", + "first prompt", + "first answer", + "second prompt", + "second answer", + ] + + def test_branch_handler_uses_merged_messaging_messages_for_keep_count(): branch_idx = ROUTES_PY.index('parsed.path == "/api/session/branch":') block = ROUTES_PY[branch_idx : branch_idx + 2600]