fix(stream): preserve repeated identical visible turns in gateway backfill (Codex MUST-FIX on #3300)

_merge_display_messages_after_agent_result shared one _emitted set between
context-backfill rows and the visible-display backbone; _message_identity
ignores timestamps, so a second identical visible user turn was suppressed as
already-emitted. Now the display backbone is always appended in order; a
separate _context_inserted set + the _display_id_set guard dedupe only the
spliced-in context rows. Adds a regression test with two identical visible
user turns + a context-only gap.
This commit is contained in:
nesquena-hermes committed 2026-06-02 18:43:25 +00:00
1 parent f07629bb4e
commit ebfff8e074
2 files changed
+89 -11

No files matched your search

+18 -11
View File
@@ -3120,7 +3120,14 @@ def _merge_display_messages_after_agent_result(previous_display, previous_contex
if _has_context_only_turns:
context_keys = [_message_identity(m) for m in previous_context]
_backfilled = []
_emitted = set()
# #3300 fix: track ONLY context rows we splice in, so the
# visible-display backbone is never suppressed. Sharing one set
# between context inserts and display rows (and _message_identity
# ignoring timestamps) dropped a legitimate second identical visible
# user turn. Display rows are always appended in order; a context
# row is backfilled only if it isn't already a display row and
# hasn't already been inserted.
_context_inserted = set()
_cursor = 0
for _display_idx, _dmsg in enumerate(previous_display):
_dkey = _message_identity(_dmsg)
@@ -3132,9 +3139,9 @@ def _merge_display_messages_after_agent_result(previous_display, previous_contex
for _k in range(_cursor, _j):
_ckey = context_keys[_k]
_cmsg = previous_context[_k]
if _ckey is not None and _ckey not in _emitted and not _is_context_compression_marker(_cmsg):
if _ckey is not None and _ckey not in _context_inserted and _ckey not in _display_id_set and not _is_context_compression_marker(_cmsg):
_backfilled.append(copy.deepcopy(_cmsg))
_emitted.add(_ckey)
_context_inserted.add(_ckey)
_cursor = _j + 1
elif not any(
_message_identity(_future_dmsg) in context_keys[_cursor:]
@@ -3143,21 +3150,21 @@ def _merge_display_messages_after_agent_result(previous_display, previous_contex
for _k in range(_cursor, len(context_keys)):
_ckey = context_keys[_k]
_cmsg = previous_context[_k]
if _ckey is not None and _ckey not in _emitted and not _is_context_compression_marker(_cmsg):
if _ckey is not None and _ckey not in _context_inserted and _ckey not in _display_id_set and not _is_context_compression_marker(_cmsg):
_backfilled.append(copy.deepcopy(_cmsg))
_emitted.add(_ckey)
_context_inserted.add(_ckey)
_cursor = len(context_keys)
if _dkey not in _emitted:
_backfilled.append(_dmsg)
if _dkey is not None:
_emitted.add(_dkey)
# The display row is the visible backbone — always preserve it,
# in order, even when an earlier (identical-content) turn or a
# backfilled context row shares its timestamp-less identity.
_backfilled.append(_dmsg)
while _cursor < len(context_keys):
_ckey = context_keys[_cursor]
_cmsg = previous_context[_cursor]
_cursor += 1
if _ckey is not None and _ckey not in _emitted and not _is_context_compression_marker(_cmsg):
if _ckey is not None and _ckey not in _context_inserted and _ckey not in _display_id_set and not _is_context_compression_marker(_cmsg):
_backfilled.append(copy.deepcopy(_cmsg))
_emitted.add(_ckey)
_context_inserted.add(_ckey)
if len(_backfilled) > len(previous_display):
logger.debug(
"Backfilled %d context-only turns into previous_display (was %d, now %d)",