From 7e8834d8ca3192e35d0399bb8d5ccf1b080e17c5 Mon Sep 17 00:00:00 2001 From: Frank Song Date: Thu, 25 Jun 2026 10:52:59 +0800 Subject: [PATCH] Fix tool-call tail ordering in session merge --- api/models.py | 12 +++++++++++- tests/test_merge_key_tool_calls.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/api/models.py b/api/models.py index f4a70db0..94cf2ef5 100644 --- a/api/models.py +++ b/api/models.py @@ -5822,7 +5822,17 @@ def merge_session_messages_append_only( if _tc: _ck = _session_message_content_key(msg) if _ck in seen_content_keys and dedup_key not in seen_dedup_keys: - pass # different tool_calls from sidecar — preserve + # Different tool_calls from sidecar — preserve, but keep + # the row in timestamp order. Falling through to the + # generic append path would move older tool-call-only + # assistant rows after the settled final answer. + if _insert_state_message_chronologically(merged_messages, msg): + seen_message_keys.add(key) + seen_dedup_keys.add(dedup_key) + seen_content_keys.add(_session_message_content_key(msg)) + seen_visible_keys.add(visible_key) + _remember_merged_message(msg) + continue else: _merge_session_display_metadata(merged_by_message_key.get(key), msg) continue diff --git a/tests/test_merge_key_tool_calls.py b/tests/test_merge_key_tool_calls.py index 40c5b4ac..db785824 100644 --- a/tests/test_merge_key_tool_calls.py +++ b/tests/test_merge_key_tool_calls.py @@ -126,6 +126,34 @@ class TestMergeToolCallsEndToEnd: } assert tc_ids == {"call_1", "call_2"} + def test_older_state_tool_call_assistant_stays_before_final_answer(self): + """Older tool-call-only state.db rows must not become the final tail. + + The WebUI sidecar can already contain a settled final answer while + state.db still has empty-content assistant rows that carry distinct + tool_calls from earlier in the turn. Those rows are real activity and + should be preserved, but appending them after the final answer makes the + renderer treat the final answer as a non-final assistant segment. + """ + sidecar_tool = _assistant_tc("call_1", "read_file", timestamp=1000.0) + final_answer = { + "role": "assistant", + "content": "Final answer is complete.", + "timestamp": 1001.0, + } + state_tool = _assistant_tc("call_2", "terminal", timestamp=1000.5) + + result = merge_session_messages_append_only( + [sidecar_tool, final_answer], + [state_tool], + ) + + assert result[-1] == final_answer + assert [m.get("tool_calls", [{}])[0].get("id") for m in result if m.get("tool_calls")] == [ + "call_1", + "call_2", + ] + def test_no_tool_calls_still_deduped(self): """Messages without tool_calls are deduplicated by legacy key as before.""" msg = {"role": "assistant", "content": "hello", "timestamp": 1000}