diff --git a/api/models.py b/api/models.py index 3216d3fa..51a70268 100644 --- a/api/models.py +++ b/api/models.py @@ -11016,6 +11016,11 @@ def _merge_session_messages_append_only_impl( _copy_api_content_sidecar(existing, msg) _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 + ) + 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 fa6661e9..6e5390b0 100644 --- a/tests/test_native_image_turn_display_context.py +++ b/tests/test_native_image_turn_display_context.py @@ -799,6 +799,76 @@ def test_marked_native_image_mirror_repairs_malformed_sidecar_once(): assert replay_rows[0]["api_content"] == api_content +def test_marked_native_image_mirror_conflict_stays_bounded_across_recovery(): + import api.models as models + + timestamp = 860.0 + session, identity, _ = _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"]) + session.messages.append({ + "role": "user", + "content": mirror, + "timestamp": timestamp, + "_state_db_row_id": 42, + "api_content": "OLD-PROVIDER-BYTES", + }) + state_row = { + "role": "user", + "content": mirror, + "timestamp": timestamp, + "_state_db_row_id": 42, + "api_content": "NEW-PROVIDER-BYTES", + } + marked = models._suppress_native_image_display_mirrors(session, [state_row]) + assert marked[0]["_webui_unmatched_native_image_mirror"] is True + + first_public = None + first_replay = None + for _ in range(4): + display = models.reconciled_state_db_messages_for_session( + session, + state_messages=[state_row], + ) + context = models.reconciled_state_db_messages_for_session( + session, + prefer_context=True, + state_messages=[state_row], + ) + display_row_42 = [ + message for message in display + if message.get("_state_db_row_id") == 42 + ] + context_row_42 = [ + message for message in context + if message.get("_state_db_row_id") == 42 + ] + assert len(display_row_42) == 2 + assert {message["api_content"] for message in display_row_42} == { + "OLD-PROVIDER-BYTES", + "NEW-PROVIDER-BYTES", + } + assert len(context_row_42) == 1 + assert context_row_42[0]["api_content"] == "NEW-PROVIDER-BYTES" + + public = public_session_projection({"messages": display})["messages"] + replay = _sanitize_messages_for_agent(context) + if first_public is None: + first_public = public + first_replay = replay + 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