Fix tool-call tail ordering in session merge

This commit is contained in:
Frank Song 2026-06-25 10:52:59 +08:00 committed by nesquena-hermes
parent 76eb548007
commit 7e8834d8ca
2 changed files with 39 additions and 1 deletions

View file

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

View file

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