diff --git a/api/models.py b/api/models.py index 909f3f61..fcd12ff5 100644 --- a/api/models.py +++ b/api/models.py @@ -3270,21 +3270,19 @@ def state_db_delta_after_context(sidecar_context: list, state_messages: list) -> if best_len < 2: return state_messages - sidecar_key_set = set(sidecar_keys) - last_represented_state_index = best_len - 1 - for idx, key in enumerate(state_keys[best_len:], start=best_len): - if key in sidecar_key_set: - last_represented_state_index = idx - - delta = [] - for msg, key in zip( - state_messages[last_represented_state_index + 1:], - state_keys[last_represented_state_index + 1:], - ): - if key in sidecar_key_set: - continue - delta.append(msg) - return delta + # Drop only rows that can be aligned with the remaining sidecar context in + # order. This still tolerates stale state-only rows between mirrored context + # rows, but once the sidecar context is exhausted every later state row is a + # real delta, even if it repeats a short earlier message. + sidecar_index = best_len + state_index = best_len + while sidecar_index < len(sidecar_keys) and state_index < len(state_keys): + if state_keys[state_index] == sidecar_keys[sidecar_index]: + sidecar_index += 1 + state_index += 1 + if sidecar_index == len(sidecar_keys): + return state_messages[state_index:] + return state_messages[best_len:] def merge_session_messages_append_only(sidecar_messages: list, state_messages: list) -> list: diff --git a/tests/test_issue1217_transcript_compaction.py b/tests/test_issue1217_transcript_compaction.py index 7b870d61..eced9261 100644 --- a/tests/test_issue1217_transcript_compaction.py +++ b/tests/test_issue1217_transcript_compaction.py @@ -4,7 +4,7 @@ import json import sys from types import SimpleNamespace -from api.models import Session, reconciled_state_db_messages_for_session +from api.models import Session, reconciled_state_db_messages_for_session, state_db_delta_after_context from api.streaming import ( _assistant_reply_added_after_current_turn, @@ -513,6 +513,26 @@ def test_prefer_context_reconcile_starts_after_last_state_row_seen_in_context(): {"role": "user", "content": "new after represented boundary", "timestamp": 200.0}, ] + + +def test_state_db_delta_preserves_fresh_rows_before_repeated_context_message(): + sidecar_context = [ + {"role": "user", "content": "ok"}, + {"role": "assistant", "content": "ready"}, + ] + state_messages = [ + {"role": "user", "content": "ok", "timestamp": 1.0}, + {"role": "assistant", "content": "ready", "timestamp": 2.0}, + {"role": "user", "content": "fresh question", "timestamp": 3.0}, + {"role": "user", "content": "ok", "timestamp": 4.0}, + {"role": "assistant", "content": "after repeat", "timestamp": 5.0}, + ] + + delta = state_db_delta_after_context(sidecar_context, state_messages) + + assert [m["content"] for m in delta] == ["fresh question", "ok", "after repeat"] + + def test_non_streaming_chat_writeback_dedupes_full_context_replay(): previous_context = [ {"role": "assistant", "content": "cron banner"},