From b5596c5acf25e58a8e592c7d2db6290e57b9d3a6 Mon Sep 17 00:00:00 2001 From: starship-s <45587122+starship-s@users.noreply.github.com> Date: Wed, 23 Sep 2026 01:39:25 -0600 Subject: [PATCH] fix(chat): preserve distinct recovered image rows Match durable row identity as well as content before skipping a recovered image row, preserving separate same-text turns. Assisted-by: Hermes Agent:gpt-6-sol Assisted-by: Codex:gpt-6-luna Assisted-by: Claude Code:claude-opus-5-5 --- api/models.py | 27 ++++++++- .../test_native_image_turn_display_context.py | 57 +++++++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/api/models.py b/api/models.py index 51a70268..1077cde7 100644 --- a/api/models.py +++ b/api/models.py @@ -11017,10 +11017,31 @@ def _merge_session_messages_append_only_impl( _merge_session_display_metadata(existing, msg) continue if dedup_key in seen_dedup_keys: - _merge_session_display_metadata( - merged_by_dedup_key.get(dedup_key), msg + duplicate = merged_by_dedup_key.get(dedup_key) + duplicate_row_id, duplicate_row_id_valid = ( + _state_db_row_identity_details(duplicate) ) - continue + same_durable_row = ( + row_id_valid + and row_id is not None + and duplicate_row_id_valid + and duplicate_row_id == row_id + ) + if not same_durable_row and row_id_valid and row_id is not None: + duplicate = merged_by_row_id.get(row_id) + duplicate_row_id, duplicate_row_id_valid = ( + _state_db_row_identity_details(duplicate) + ) + same_durable_row = ( + row_id not in ambiguous_row_ids + and duplicate_row_id_valid + and duplicate_row_id == row_id + and _cached_message_key(duplicate, "dedup") == dedup_key + and _row_id_fast_path_allowed(duplicate, msg) + ) + if same_durable_row: + _merge_session_display_metadata(duplicate, msg) + continue if not _insert_state_message_chronologically(merged_messages, msg): merged_messages.append(msg) seen_message_keys.add(key) diff --git a/tests/test_native_image_turn_display_context.py b/tests/test_native_image_turn_display_context.py index 6e5390b0..a04b145f 100644 --- a/tests/test_native_image_turn_display_context.py +++ b/tests/test_native_image_turn_display_context.py @@ -869,6 +869,63 @@ def test_marked_native_image_mirror_conflict_stays_bounded_across_recovery(): session.context_messages = context +def test_marked_native_image_rows_with_distinct_ids_survive_recovery(): + import api.models as models + + timestamp = 870.0 + session, identity, api_content = _settle_image_turn( + timestamp=timestamp, + agent_row_id=41, + ) + context_user = next( + message for message in session.context_messages + if message.get("_active_turn_token") == identity["token"] + ) + mirror = _durable_agent_content(context_user["content"]) + state_rows = [ + { + "role": "user", + "content": mirror, + "timestamp": timestamp, + "_state_db_row_id": row_id, + "api_content": api_content, + } + for row_id in (42, 43) + ] + + first_public = None + first_replay = None + for _ in range(4): + display = models.reconciled_state_db_messages_for_session( + session, + state_messages=state_rows, + ) + context = models.reconciled_state_db_messages_for_session( + session, + prefer_context=True, + state_messages=state_rows, + ) + for messages in (display, context): + rows = [ + message for message in messages + if message.get("_state_db_row_id") in (42, 43) + ] + assert [message["_state_db_row_id"] for message in rows] == [42, 43] + assert all(message["api_content"] == api_content for message in rows) + + public = public_session_projection({"messages": display})["messages"] + replay = _sanitize_messages_for_agent(context) + if first_public is None: + first_public = public + first_replay = replay + assert sum(message.get("content") == mirror for message in public) == 2 + else: + assert public == first_public + assert replay == first_replay + session.messages = display + session.context_messages = context + + def test_unlinked_state_db_image_projection_uses_existing_reconciliation(): import api.models as models